Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Concurrent resolution #6

Closed
wants to merge 17 commits into from
Closed
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -12,3 +12,5 @@

# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
.glide/

bin/
7 changes: 5 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -3,5 +3,8 @@ sudo: false
go:
- 1.7.x
- 1.8.x

script: go test $(go list ./... | grep -v vendor/)
before_install:
- go get github.com/mattn/goveralls
script:
- $HOME/gopath/bin/goveralls -service=travis-ci
#script: go test $(go list ./... | grep -v vendor/)
29 changes: 18 additions & 11 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
# Builds and installs the depth CLI.
install:
@go install -v github.com/KyleBanks/depth/cmd/depth
@echo "depth installed."
.PHONY: install
VERSION = 1.1.0

RELEASE_PKG = ./cmd/depth
INSTALL_PKG = $(RELEASE_PKG)


# Remote includes require 'mmake'
# github.com/tj/mmake
include github.com/KyleBanks/make/go/install
include github.com/KyleBanks/make/go/sanity
include github.com/KyleBanks/make/go/release
include github.com/KyleBanks/make/go/bench
include github.com/KyleBanks/make/git/precommit

# Runs a number of depth commands as examples of what's possible.
example: | install
depth github.com/KyleBanks/depth/cmd/depth strings
depth github.com/KyleBanks/depth/cmd/depth strings ./

depth -internal strings

@@ -17,9 +25,8 @@ example: | install
depth -test -internal strings

depth -test -internal -max 3 strings
.PHONY: example

include github.com/KyleBanks/make/misc/precommit
include github.com/KyleBanks/make/go/sanity
include github.com/KyleBanks/make/go/release
include github.com/KyleBanks/make/go/bench
depth .

depth ./cmd/depth
.PHONY: example
34 changes: 30 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -2,12 +2,15 @@

[![GoDoc](https://godoc.org/github.com/KyleBanks/depth?status.svg)](https://godoc.org/github.com/KyleBanks/depth) 
[![Build Status](https://travis-ci.org/KyleBanks/depth.svg?branch=master)](https://travis-ci.org/KyleBanks/depth) 
[![Go Report Card](https://goreportcard.com/badge/github.com/KyleBanks/depth)](https://goreportcard.com/report/github.com/KyleBanks/depth)
[![Go Report Card](https://goreportcard.com/badge/github.com/KyleBanks/depth)](https://goreportcard.com/report/github.com/KyleBanks/depth) 
[![Coverage Status](https://coveralls.io/repos/github/KyleBanks/depth/badge.svg?branch=master)](https://coveralls.io/github/KyleBanks/depth?branch=master)

`depth` is tool to retrieve and visualize Go source code dependency trees.

## Install

Download the appropriate binary for your platform from the [Releases](https://github.com/KyleBanks/depth/releases) page, or:

```sh
go get github.com/KyleBanks/depth/cmd/depth
```
@@ -18,7 +21,7 @@ go get github.com/KyleBanks/depth/cmd/depth

### Command-Line

Simply execute `depth` with one or more package names to visualize:
Simply execute `depth` with one or more package names to visualize. You can use the fully qualified import path of the package, like so:

```sh
$ depth github.com/KyleBanks/depth/cmd/depth
@@ -38,6 +41,14 @@ github.com/KyleBanks/depth/cmd/depth
└ strings
```

Or you can use a relative path, for example:

```sh
$ depth .
$ depth ./cmd/depth
$ depth ../
```

You can also use `depth` on the Go standard library:

```sh
@@ -52,7 +63,7 @@ strings
Visualizing multiple packages at a time is supported by simply naming the packages you'd like to visualize:

```sh
$ depth strings github.com/KyleBanks/depth
$ depth strings github.com/KyleBanks/depth
strings
├ errors
├ io
@@ -176,10 +187,25 @@ if err != nil {
log.Printf("'%v' has %v dependencies.", t.Root.Name, len(t.Root.Deps))
```

For additional customization, simply set the appropriate flags on the `Tree` before resolving:

```go
import "github.com/KyleBanks/depth"

t := depth.Tree {
ResolveInternal: true,
ResolveTest: true,
MaxDepth: 10,
}


err := t.Resolve("strings")
```

## Author

`depth` was developed by [Kyle Banks](https://twitter.com/kylewbanks).

## License

`depth` is available under [MIT](./LICENSE)
`depth` is available under the [MIT](./LICENSE) license.
15 changes: 6 additions & 9 deletions benchmark/benchmark_test.go → bench_test.go
Original file line number Diff line number Diff line change
@@ -1,36 +1,33 @@
package benchmark
package depth

import (
"testing"

"github.com/KyleBanks/depth"
)

func BenchmarkTree_ResolveStrings(b *testing.B) {
var t depth.Tree
benchmarkTreeResolveStrings(&t, b)
benchmarkTreeResolveStrings(&Tree{}, b)
}

func BenchmarkTree_ResolveStringsInternal(b *testing.B) {
benchmarkTreeResolveStrings(&depth.Tree{
benchmarkTreeResolveStrings(&Tree{
ResolveInternal: true,
}, b)
}

func BenchmarkTree_ResolveStringsTest(b *testing.B) {
benchmarkTreeResolveStrings(&depth.Tree{
benchmarkTreeResolveStrings(&Tree{
ResolveTest: true,
}, b)
}

func BenchmarkTree_ResolveStringsInternalTest(b *testing.B) {
benchmarkTreeResolveStrings(&depth.Tree{
benchmarkTreeResolveStrings(&Tree{
ResolveInternal: true,
ResolveTest: true,
}, b)
}

func benchmarkTreeResolveStrings(t *depth.Tree, b *testing.B) {
func benchmarkTreeResolveStrings(t *Tree, b *testing.B) {
for i := 0; i < b.N; i++ {
if err := t.Resolve("strings"); err != nil {
b.Fatal(err)
88 changes: 88 additions & 0 deletions cmd/depth/depth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package main

import (
"encoding/json"
"flag"
"fmt"
"io"
"os"
"strings"

"github.com/KyleBanks/depth"
)

const (
outputPadding = " "
outputPrefix = "├ "
outputPrefixLast = "└ "
)

var outputJSON bool

func main() {
t := parse(os.Args[1:])
if err := handlePkgs(t, flag.Args(), outputJSON); err != nil {
os.Exit(1)
}
}

// parse constructs a depth.Tree from command-line arguments.
func parse(args []string) *depth.Tree {
f := flag.NewFlagSet(os.Args[0], flag.ExitOnError)

var t depth.Tree
f.BoolVar(&t.ResolveInternal, "internal", false, "If set, resolves dependencies of internal (stdlib) packages.")
f.BoolVar(&t.ResolveTest, "test", false, "If set, resolves dependencies used for testing.")
f.IntVar(&t.MaxDepth, "max", 0, "Sets the maximum depth of dependencies to resolve.")
f.BoolVar(&outputJSON, "json", false, "If set, outputs the depencies in JSON format.")
f.Parse(args)

return &t
}

// handlePkgs takes a slice of package names, resolves a Tree on them,
// and outputs each Tree to Stdout.
func handlePkgs(t *depth.Tree, pkgs []string, outputJSON bool) error {
for _, pkg := range pkgs {
err := t.Resolve(pkg)
if err != nil {
fmt.Printf("'%v': FATAL: %v\n", pkg, err)
return err
}

if outputJSON {
writePkgJSON(os.Stdout, *t.Root)
continue
}

writePkg(os.Stdout, *t.Root, 0, false)
}

return nil
}

// writePkgJSON writes the full Pkg as JSON to the provided Writer.
func writePkgJSON(w io.Writer, p depth.Pkg) {
e := json.NewEncoder(w)
e.SetIndent("", " ")
e.Encode(p)
}

// writePkg recursively prints a Pkg and its dependencies to the Writer provided.
func writePkg(w io.Writer, p depth.Pkg, indent int, isLast bool) {
var prefix string
if indent > 0 {
prefix = outputPrefix

if isLast {
prefix = outputPrefixLast
}
}

out := fmt.Sprintf("%v%v%v\n", strings.Repeat(outputPadding, indent), prefix, p.String())
w.Write([]byte(out))

for idx, d := range p.Deps {
writePkg(w, d, indent+1, idx == len(p.Deps)-1)
}
}
137 changes: 137 additions & 0 deletions cmd/depth/depth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package main

import (
"fmt"
"testing"

"github.com/KyleBanks/depth"
)

func Test_parse(t *testing.T) {
tests := []struct {
internal bool
test bool
depth int
json bool
}{
{true, true, 0, true},
{false, false, 10, false},
{true, false, 10, false},
{false, true, 5, true},
}

for idx, tt := range tests {
tr := parse([]string{
fmt.Sprintf("-internal=%v", tt.internal),
fmt.Sprintf("-test=%v", tt.test),
fmt.Sprintf("-max=%v", tt.depth),
fmt.Sprintf("-json=%v", tt.json),
})

if tr.ResolveInternal != tt.internal {
t.Fatalf("[%v] Unexpected ResolveInternal, expected=%v, got=%v", idx, tt.internal, tr.ResolveInternal)
} else if tr.ResolveTest != tt.test {
t.Fatalf("[%v] Unexpected ResolveTest, expected=%v, got=%v", idx, tt.test, tr.ResolveTest)
} else if tr.MaxDepth != tt.depth {
t.Fatalf("[%v] Unexpected MaxDepth, expected=%v, got=%v", idx, tt.depth, tr.MaxDepth)
} else if outputJSON != tt.json {
t.Fatalf("[%v] Unexpected outputJSON, expected=%v, got=%v", idx, tt.json, outputJSON)
}
}
}

func Example_handlePkgsStrings() {
var t depth.Tree

handlePkgs(&t, []string{"strings"}, false)
// Output:
// strings
// ├ errors
// ├ io
// ├ unicode
// └ unicode/utf8
}

func Example_handlePkgsTestStrings() {
var t depth.Tree
t.ResolveTest = true

handlePkgs(&t, []string{"strings"}, false)
// Output:
// strings
// ├ bytes
// ├ errors
// ├ fmt
// ├ io
// ├ io/ioutil
// ├ math/rand
// ├ reflect
// ├ sync
// ├ testing
// ├ unicode
// ├ unicode/utf8
// └ unsafe
}

func Example_handlePkgsDepth() {
var t depth.Tree

handlePkgs(&t, []string{"github.com/KyleBanks/depth/cmd/depth"}, false)
// Output:
// github.com/KyleBanks/depth/cmd/depth
// ├ encoding/json
// ├ flag
// ├ fmt
// ├ io
// ├ os
// ├ strings
// └ github.com/KyleBanks/depth
// ├ bytes
// ├ errors
// ├ go/build
// ├ os
// ├ path
// ├ sort
// └ strings
}

func Example_handlePkgsUnknown() {
var t depth.Tree

handlePkgs(&t, []string{"notreal"}, false)
// Output:
// 'notreal': FATAL: unable to resolve root package
}

func Example_handlePkgsJson() {
var t depth.Tree
handlePkgs(&t, []string{"strings"}, true)

// Output:
// {
// "name": "strings",
// "resolved": true,
// "deps": [
// {
// "name": "errors",
// "resolved": true,
// "deps": null
// },
// {
// "name": "io",
// "resolved": true,
// "deps": null
// },
// {
// "name": "unicode",
// "resolved": true,
// "deps": null
// },
// {
// "name": "unicode/utf8",
// "resolved": true,
// "deps": null
// }
// ]
// }
}
64 changes: 47 additions & 17 deletions depth.go
Original file line number Diff line number Diff line change
@@ -3,25 +3,40 @@
//
// For example, the dependencies of the stdlib `strings` package can be resolved like so:
//
// ```go
// import "github.com/KyleBanks/depth"
// import "github.com/KyleBanks/depth"
//
// var t depth.Tree
// err := t.Resolve("strings")
// if err != nil {
// log.Fatal(err)
// }
// var t depth.Tree
// err := t.Resolve("strings")
// if err != nil {
// log.Fatal(err)
// }
//
// // Output: "strings has 4 dependencies."
// log.Printf("%v has %v dependencies.", t.Root.Name, len(t.Root.Deps))
// ```
// // Output: "strings has 4 dependencies."
// log.Printf("%v has %v dependencies.", t.Root.Name, len(t.Root.Deps))
//
// For additional customization, simply set the appropriate flags on the `Tree` before resolving:
//
// import "github.com/KyleBanks/depth"
//
// t := depth.Tree {
// ResolveInternal: true,
// ResolveTest: true,
// MaxDepth: 10,
// }
//
// err := t.Resolve("strings")
package depth

import (
"errors"
"go/build"
"sync"
"os"
)

// ErrRootPkgNotResolved is returned when the root Pkg of the Tree cannot be resolved,
// typically because it does not exist.
var ErrRootPkgNotResolved = errors.New("unable to resolve root package")

// Importer defines a type that can import a package and return its details.
type Importer interface {
Import(name, srcDir string, im build.ImportMode) (*build.Package, error)
@@ -38,20 +53,38 @@ type Tree struct {

Importer Importer

mu sync.Mutex
importCache map[string]struct{}
}

// Resolve recursively finds all dependencies for the root Pkg name provided,
// and the packages it depends on.
func (t *Tree) Resolve(name string) error {
t.Root = &Pkg{Name: name, Tree: t}
pwd, err := os.Getwd()
if err != nil {
return err
}

t.Root = &Pkg{
Name: name,
Tree: t,
SrcDir: pwd,
}

// Reset the import cache each time to ensure a reused Tree doesn't
// reuse the same cache.
t.importCache = nil

// Allow custom importers, but use build.Default if none is provided.
if t.Importer == nil {
t.Importer = &build.Default
}

return t.Root.Resolve(t.Importer, true)
t.Root.Resolve(t.Importer)
if !t.Root.Resolved {
return ErrRootPkgNotResolved
}

return nil
}

// shouldResolveInternal determines if internal packages should be further resolved beyond the
@@ -84,9 +117,6 @@ func (t *Tree) isAtMaxDepth(p *Pkg) bool {
// hasSeenImport returns true if the import name provided has already been seen within the tree.
// This function only returns false for a name once.
func (t *Tree) hasSeenImport(name string) bool {
t.mu.Lock()
defer t.mu.Unlock()

if t.importCache == nil {
t.importCache = make(map[string]struct{})
}
31 changes: 31 additions & 0 deletions depth_test.go
Original file line number Diff line number Diff line change
@@ -13,6 +13,37 @@ func (m MockImporter) Import(name, srcDir string, im build.ImportMode) (*build.P
return m.ImportFn(name, srcDir, im)
}

func TestTree_Resolve(t *testing.T) {
// Fail case, bad package name
var tr Tree
if err := tr.Resolve("name"); err != ErrRootPkgNotResolved {
t.Fatalf("Unexpected error, expected=%v, got=%b", ErrRootPkgNotResolved, err)
}

// Positive case, expect deps
if err := tr.Resolve("strings"); err != nil {
t.Fatal(err)
}

if tr.Root == nil || tr.Root.Name != "strings" {
t.Fatalf("Unexpected Root, expected=%v, got=%v", "strings", tr.Root)
} else if len(tr.Root.Deps) == 0 {
t.Fatal("Expected positive number of Deps")
} else if len(tr.Root.SrcDir) == 0 {
t.Fatal("Expected SrcDir to be populated")
}

// Reuse the same tree and the same package to ensure that the internal pkg cache
// is reset and dependencies are still resolved.
stringsDepCount := len(tr.Root.Deps)
if err := tr.Resolve("strings"); err != nil {
t.Fatal(err)
}
if len(tr.Root.Deps) != stringsDepCount {
t.Fatalf("Unexpected number of Deps, expected=%v, got=%b", stringsDepCount, len(tr.Root.Deps))
}
}

func TestTree_shouldResolveInternal(t *testing.T) {
var pt Tree
pt.Root = &Pkg{}
90 changes: 39 additions & 51 deletions pkg.go
Original file line number Diff line number Diff line change
@@ -1,53 +1,58 @@
package depth

import (
"bytes"
"go/build"
"path"
"sort"
"strings"
"sync"
)

// Pkg represents a Go source package, and its dependencies.
type Pkg struct {
Name string `json:"name"`
SrcDir string `json:"-"`
Internal bool `json:"-"`
Name string `json:"name"`
SrcDir string `json:"-"`

Internal bool `json:"-"`
Resolved bool `json:"resolved"`

Tree *Tree `json:"-"`
Parent *Pkg `json:"-"`
mu *sync.Mutex
Deps []Pkg `json:"deps"`
}

// Resolve recursively finds all dependencies for the Pkg and the packages it depends on.
func (p *Pkg) Resolve(i Importer, resolveImports bool) error {
func (p *Pkg) Resolve(i Importer) {
// Resolved is always true, regardless of if we skip the import,
// it is only false if there is an error while importing.
p.Resolved = true

name := p.cleanName()
if name == "" {
return nil
}

// Stop resolving imports if we've reached max depth.
if resolveImports && p.Tree.isAtMaxDepth(p) {
resolveImports = false
return
}

// Stop resolving imports if we've reached max depth or found a duplicate.
var importMode build.ImportMode
if !resolveImports {
if p.Tree.hasSeenImport(name) || p.Tree.isAtMaxDepth(p) {
importMode = build.FindOnly
}

pkg, err := i.Import(name, p.SrcDir, importMode)
if err != nil {
return err
// TODO: Check the error type?
p.Resolved = false
return
}

// Update the name with the fully qualified import path.
p.Name = pkg.ImportPath

// If this is an internal dependency, we may need to skip it.
if pkg.Goroot {
p.Internal = true

if !p.Tree.shouldResolveInternal(p) {
return nil
return
}
}

@@ -56,72 +61,44 @@ func (p *Pkg) Resolve(i Importer, resolveImports bool) error {
imports = append(imports, append(pkg.TestImports, pkg.XTestImports...)...)
}

return p.setDeps(i, imports, pkg.Dir)
p.setDeps(i, imports, pkg.Dir)
}

// setDeps takes a slice of import paths and the source directory they are relative to,
// and creates the Deps of the Pkg. Each dependency is also further resolved prior to being added
// to the Pkg.
func (p *Pkg) setDeps(i Importer, imports []string, srcDir string) error {
p.mu = &sync.Mutex{}
func (p *Pkg) setDeps(i Importer, imports []string, srcDir string) {
unique := make(map[string]struct{})
errCh := make(chan error)
var wg sync.WaitGroup

for _, imp := range imports {
// Mostly for testing files where cyclic imports are allowed.
if imp == p.Name {
continue
}

// Skip duplicates.
if _, ok := unique[imp]; ok {
continue
}
unique[imp] = struct{}{}

wg.Add(1)
go func(imp string) {
err := p.addDep(i, imp, srcDir)

wg.Done()
if err != nil {
errCh <- err
}
}(imp)
p.addDep(i, imp, srcDir)
}

wg.Wait()

select {
case err := <-errCh:
return err
default:
sort.Sort(byInternalAndName(p.Deps))
return nil
}
sort.Sort(byInternalAndName(p.Deps))
}

// addDep creates a Pkg and it's dependencies from an imported package name.
func (p *Pkg) addDep(i Importer, name string, srcDir string) error {
// Don't resolve imports for Pkgs that we've already seen and resolved.
resolveImports := !p.Tree.hasSeenImport(name)

func (p *Pkg) addDep(i Importer, name string, srcDir string) {
dep := Pkg{
Name: name,
SrcDir: srcDir,
Tree: p.Tree,
Parent: p,
}
dep.Resolve(i)

if err := dep.Resolve(i, resolveImports); err != nil {
return err
}

p.mu.Lock()
p.Deps = append(p.Deps, dep)
p.mu.Unlock()

return nil
}

// isParent goes recursively up the chain of Pkgs to determine if the name provided is ever a
@@ -169,6 +146,17 @@ func (p *Pkg) cleanName() string {
return name
}

// String returns a string representation of the Pkg containing the Pkg name and status.
func (p *Pkg) String() string {
b := bytes.NewBufferString(p.Name)

if !p.Resolved {
b.Write([]byte(" (unresolved)"))
}

return b.String()
}

// byInternalAndName ensures a slice of Pkgs are sorted such that the internal stdlib
// packages are always above external packages (ie. github.com/whatever).
type byInternalAndName []Pkg
9 changes: 3 additions & 6 deletions pkg_test.go
Original file line number Diff line number Diff line change
@@ -32,6 +32,7 @@ func TestPkg_CleanName(t *testing.T) {
func TestPkg_AddDepImportSeen(t *testing.T) {
var m MockImporter
var tr Tree
tr.Importer = m

testName := "test"
testSrcDir := "src/testing"
@@ -53,15 +54,11 @@ func TestPkg_AddDepImportSeen(t *testing.T) {
}

// Hasn't seen the import
if err := p.addDep(m, testName, testSrcDir); err != nil {
t.Fatal(err)
}
p.addDep(m, testName, testSrcDir)

// Has seen the import
expectedIm = build.FindOnly
if err := p.addDep(m, testName, testSrcDir); err != nil {
t.Fatal(err)
}
p.addDep(m, testName, testSrcDir)
}

func TestByInternalAndName(t *testing.T) {