├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── cmd ├── apply.go ├── apply_test.go ├── cmd.go ├── revert.go ├── revert_test.go ├── root.go ├── root_test.go ├── visualize.go └── visualize_test.go ├── examples ├── example1.png └── example2.png ├── go.mod ├── go.sum ├── parser ├── parser.go ├── parser_test.go └── parserfakes │ └── fake_parser.go ├── printracer.go ├── tracing ├── deinstrument.go ├── deinstrument_test.go ├── instrument.go ├── instrument_test.go ├── interfaces.go ├── tracingfakes │ ├── fake_code_deinstrumenter.go │ ├── fake_code_instrumenter.go │ └── fake_imports_groomer.go ├── unused_imports.go ├── unused_imports_test.go └── util.go └── vis ├── vis.go ├── vis_test.go └── visfakes └── fake_visualizer.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | printracer 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.13.x 5 | 6 | install: 7 | - go get golang.org/x/tools/cmd/cover 8 | - go get github.com/mattn/goveralls 9 | - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.23.6 10 | 11 | script: 12 | - go test ./... -v -coverpkg $(go list ./... | egrep -v "fakes|test" | paste -sd "," -) -covermode=count -coverprofile=coverage.out 13 | - cat coverage.out | grep -v "printracer.go" > cover.out 14 | - goveralls -coverprofile=cover.out -service=travis-ci 15 | - golangci-lint run 16 | 17 | - CGO_ENABLED=0 GOARCH=386 GOOS=linux go build -o out/printracer_linux_i686 . 18 | - CGO_ENABLED=0 GOARCH=amd64 GOOS=linux go build -o out/printracer_linux_x86-64 . 19 | - GOARCH=amd64 GOOS=darwin go build -o out/printracer_osx . 20 | - GOARCH=386 GOOS=windows go build -o out/printracer_win32.exe . 21 | - GOARCH=amd64 GOOS=windows go build -o out/printracer_win64.exe . 22 | 23 | deploy: 24 | provider: releases 25 | api_key: 26 | secure: "I0rhQGOIZs1mW87o/72zq2eTo6Iqsl/GXh/M51GDXoatkpHYI2lyBKkIKhOJztl91nPFxBMX+p401lf8zFQR1tZprps2fsoMnA33ia3vIW+fkueUBrdhL7zWiXwRh0ASN1MYRMkrhsHRuO8rmhUNw7jls1HXA4sZhQOD6H8GFfA53d0a+9n5Xif8eZGLgjQ9rZGVm7KyHP2PL9oJVcb7PE7JkZ8DsrdZscY+PFeWRjov8oFu48QmccXZqUwTyJ6qx+ycVYZoEtTs/cGnoqCMhx1Q2DuOxzLex52dudIxCBxk9bV7d609xY3zxcOeHqdv4QNVNqfaQlc/W2jqAvHC0x78IVN8CKWBzGYM8vHWR+JFBbRDWdDa++KeUTS1OcfRSRMUChF3WxjNZWGQ04u8zQ/75Z6ar4Q2bePAoC66tkrUVVye40SToYuT4fNezEITFdFNcFvBQIrI9zVhdwlpQ81lxToeQXHNDJ7nyAGkz5CMlHbTW7jbHYPUvm4WixqgFoRQjrZ13eQ6Aj66WXe6EPPSjMORDNhHRYc1S1q51Ru1qSD8lSi+anNOMKnSF3sURMgPeE6oeFr8OtPwoWxTZS07IMsYlcQIETTNvoajH5gqe0XY7tTMPVBOhyg+werpss2FkYfwOPBIpKAO6XvRPMR3fyoju15l0n5A7kNipVU=" 27 | file_glob: true 28 | file: out/* 29 | skip_cleanup: true 30 | on: 31 | tags: true -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Dimitar Petrov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # prinTracer 2 | [](https://travis-ci.org/DimitarPetrov/printracer) 3 | [](https://coveralls.io/github/DimitarPetrov/printracer?branch=master) 4 | [](https://goreportcard.com/report/github.com/DimitarPetrov/printracer) 5 | 6 | ## Overview 7 | 8 | `printracer` is a simple command line tool that instruments all **go** code in the current working directory to print every 9 | function execution along with its arguments. 10 | 11 | ## Installation 12 | 13 | #### Installing from Source 14 | ``` 15 | go get -u github.com/DimitarPetrov/printracer 16 | ``` 17 | Or you can download a binary for your system [here](https://github.com/DimitarPetrov/printracer/releases). 18 | 19 | ## Demonstration 20 | 21 | ### Code Instrumentation 22 | 23 | Let's say you have a simple `main.go` file in the current working directory with the following contents: 24 | ```go 25 | package main 26 | 27 | func test(i int, b bool) int { 28 | if b { 29 | return i 30 | } 31 | return 0 32 | } 33 | 34 | func main() { 35 | _ = test(2, false) 36 | } 37 | ``` 38 | 39 | After executing: 40 | ``` 41 | printracer apply 42 | ``` 43 | 44 | The file will be modified like the following: 45 | ```go 46 | package main 47 | 48 | import ( 49 | "crypto/rand" 50 | "fmt" 51 | rt "runtime" 52 | ) 53 | 54 | func test(i int, b bool) int { 55 | 56 | /* prinTracer */ 57 | funcName := "test" 58 | caller := "unknown" 59 | if funcPC, _, _, ok := rt.Caller(0); ok { 60 | funcName = rt.FuncForPC(funcPC).Name() 61 | } 62 | if callerPC, _, _, ok := rt.Caller(1); ok { 63 | caller = rt.FuncForPC(callerPC).Name() 64 | } 65 | idBytes := make([]byte, 16) 66 | _, _ = rand.Read(idBytes) 67 | callID := fmt.Sprintf("%x-%x-%x-%x-%x", idBytes[0:4], idBytes[4:6], idBytes[6:8], idBytes[8:10], idBytes[10:]) 68 | fmt.Printf("Entering function %s called by %s with args (%v) (%v); callID=%s\n", funcName, caller, i, b, callID) 69 | defer fmt.Printf("Exiting function %s called by %s; callID=%s\n", funcName, caller, callID) /* prinTracer */ 70 | 71 | if b { 72 | return i 73 | } 74 | return 0 75 | } 76 | 77 | func main() { 78 | 79 | /* prinTracer */ 80 | funcName := "main" 81 | caller := "unknown" 82 | if funcPC, _, _, ok := rt.Caller(0); ok { 83 | funcName = rt.FuncForPC(funcPC).Name() 84 | } 85 | if callerPC, _, _, ok := rt.Caller(1); ok { 86 | caller = rt.FuncForPC(callerPC).Name() 87 | } 88 | idBytes := make([]byte, 16) 89 | _, _ = rand.Read(idBytes) 90 | callID := fmt.Sprintf("%x-%x-%x-%x-%x", idBytes[0:4], idBytes[4:6], idBytes[6:8], idBytes[8:10], idBytes[10:]) 91 | fmt.Printf("Entering function %s called by %s; callID=%s\n", funcName, caller, callID) 92 | defer fmt.Printf("Exiting function %s called by %s; callID=%s\n", funcName, caller, callID) /* prinTracer */ 93 | 94 | _ = test(2, false) 95 | } 96 | ``` 97 | When running the instrumented file above the output (so called trace) will be as follows: 98 | ``` 99 | Entering function main.main called by runtime.main; callID=0308fc13-5b30-5871-9101-b84e055a9565 100 | Entering function main.test called by main.main with args (2) (false); callID=1a3feff5-844b-039c-6d20-307d52002ce8 101 | Exiting function main.test called by main.main; callID=1a3feff5-844b-039c-6d20-307d52002ce8 102 | Exiting function main.main called by runtime.main; callID=0308fc13-5b30-5871-9101-b84e055a9565 103 | ``` 104 | 105 | You can also easily revert all the changes done by `printracer` by just executing: 106 | ``` 107 | printracer revert 108 | ``` 109 | 110 | > NOTE: `printracer revert` reverts changes only if code block enclosed by /* prinTracer */ comments is not modified by hand. If you modify the instrumentation block then it should be manually reverted afterwards. 111 | 112 | > NOTE: `printracer apply` will not apply any changes if find /* prinTracer */ comment directly above first statement in the function's body. This is needed to mitigate accidental multiple instrumentation which will then affect deinstrumentation and visualization negatively. 113 | You also can use it to signal that a particular function should not be instrumented. 114 | ### Visualization 115 | 116 | Let's say you have instrumented your code and captured the flow that is so hard to follow even the textual trace is confusing as hell. 117 | That's where visualization comes to rescue. 118 | 119 | For example let's say you have captured the following trace and saved it to the file **trace.txt**: 120 | ```text 121 | Entering function main.main called by runtime.main; callID=ec57b80b-6898-75cc-1dea-e623e7ac26c9 122 | Entering function main.foo called by main.main with args (5) (false); callID=351b3edb-7ad3-2f88-1a9b-488debf800cc 123 | Entering function main.bar called by main.foo with args (test string); callID=1e3e0e73-e4f1-b3f9-6bf5-e0aa15ddd6d1 124 | Entering function main.baz called by main.bar; callID=e1e79e3b-d89f-6e4e-e0bf-eea54db5b569 125 | Exiting function main.baz called by main.bar; callID=e1e79e3b-d89f-6e4e-e0bf-eea54db5b569 126 | Exiting function main.bar called by main.foo; callID=1e3e0e73-e4f1-b3f9-6bf5-e0aa15ddd6d1 127 | Exiting function main.foo called by main.main; callID=351b3edb-7ad3-2f88-1a9b-488debf800cc 128 | Exiting function main.main called by runtime.main; callID=ec57b80b-6898-75cc-1dea-e623e7ac26c9 129 | ``` 130 | 131 | In practice this would be much more complicated but it is enough for the sake of demonstration. 132 | 133 | Now when you execute: 134 | ``` 135 | printracer visualize trace.txt 136 | ``` 137 | A file with name `calls.html` will be generated 138 | 139 | > NOTE: The name of the file can be provided via -o (--output) flag to the visualize command. 140 | 141 |  142 | 143 | But in some situation this diagram can become so big that it becomes useless. 144 | 145 | That's where `--depth (-d)` and `--func (-f)` flags comes to rescue. 146 | 147 | - `--depth` flag controls how deep in the invocation graph you want your visualization to go. 148 | - `--func` flag controls which function to be the starting point of the visualization. 149 | 150 | > NOTE: If `--depth/--func` flags are used visualization will be linear following the call stack of the starting func. Calls from different Goroutines will be ignored! 151 | 152 | So if you execute the following command with the trace of the previous example: 153 | ``` 154 | printracer visualize trace.txt --depth 2 --func main.foo 155 | ``` 156 | A diagram like this will be generated for you: 157 | 158 |  159 | 160 | Much cleaner and focused on the let's say problematic part of the trace. 161 | 162 | 163 | -------------------------------------------------------------------------------- /cmd/apply.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/DimitarPetrov/printracer/tracing" 6 | "github.com/spf13/cobra" 7 | "os" 8 | ) 9 | 10 | type ApplyCmd struct { 11 | instrumenter tracing.CodeInstrumenter 12 | importsGroomer tracing.ImportsGroomer 13 | } 14 | 15 | func NewApplyCmd(instrumenter tracing.CodeInstrumenter, importsGroomer tracing.ImportsGroomer) *ApplyCmd { 16 | return &ApplyCmd{ 17 | instrumenter: instrumenter, 18 | importsGroomer: importsGroomer, 19 | } 20 | } 21 | 22 | func (ac *ApplyCmd) Prepare() *cobra.Command { 23 | return &cobra.Command{ 24 | Use: "apply", 25 | Aliases: []string{"a"}, 26 | Short: "Instruments a directory of go files", 27 | PreRunE: commonPreRunE(ac), 28 | RunE: commonRunE(ac), 29 | SilenceUsage: true, 30 | } 31 | } 32 | 33 | func (ac *ApplyCmd) Run() error { 34 | wd, err := os.Getwd() 35 | if err != nil { 36 | return fmt.Errorf("error getting current working directory: %v", err) 37 | } 38 | 39 | return mapDirectory(wd, func(path string) error { 40 | err := ac.instrumenter.InstrumentDirectory(path) 41 | if err != nil { 42 | return err 43 | } 44 | return ac.importsGroomer.RemoveUnusedImportFromDirectory(path, map[string]string{"fmt": "", "runtime": "rt", "rand": ""}) // TODO: flag for import aliases 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /cmd/apply_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "github.com/DimitarPetrov/printracer/tracing/tracingfakes" 6 | "testing" 7 | ) 8 | 9 | func TestApplyCmd(t *testing.T) { 10 | fakeInstrumenter := &tracingfakes.FakeCodeInstrumenter{} 11 | fakeImportsGroomer := &tracingfakes.FakeImportsGroomer{} 12 | cmd := NewApplyCmd(fakeInstrumenter, fakeImportsGroomer).Prepare() 13 | 14 | if err := cmd.Execute(); err != nil { 15 | t.Fatal(err) 16 | } 17 | } 18 | 19 | func TestApplyCmdReturnsErrorWhenInstrumenterReturnError(t *testing.T) { 20 | fakeInstrumenter := &tracingfakes.FakeCodeInstrumenter{} 21 | fakeImportsGroomer := &tracingfakes.FakeImportsGroomer{} 22 | cmd := NewApplyCmd(fakeInstrumenter, fakeImportsGroomer).Prepare() 23 | 24 | expectedErr := errors.New("error") 25 | fakeInstrumenter.InstrumentDirectoryReturns(expectedErr) 26 | 27 | if err := cmd.Execute(); err != expectedErr { 28 | t.Error("Assertion failed!") 29 | } 30 | } 31 | 32 | func TestApplyCmdReturnsErrorWhenImportsGroomerReturnError(t *testing.T) { 33 | fakeInstrumenter := &tracingfakes.FakeCodeInstrumenter{} 34 | fakeImportsGroomer := &tracingfakes.FakeImportsGroomer{} 35 | cmd := NewApplyCmd(fakeInstrumenter, fakeImportsGroomer).Prepare() 36 | 37 | expectedErr := errors.New("error") 38 | fakeImportsGroomer.RemoveUnusedImportFromDirectoryReturns(expectedErr) 39 | 40 | if err := cmd.Execute(); err != expectedErr { 41 | t.Error("Assertion failed!") 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /cmd/cmd.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "os" 6 | "path/filepath" 7 | ) 8 | 9 | type Command interface { 10 | Prepare() *cobra.Command 11 | Run() error 12 | } 13 | 14 | type CommandWithArgs interface { 15 | Validate([]string) error 16 | } 17 | 18 | func commonRunE(cmd Command) func(*cobra.Command, []string) error { 19 | return func(c *cobra.Command, args []string) error { 20 | return cmd.Run() 21 | } 22 | } 23 | 24 | func commonPreRunE(cmd Command) func(*cobra.Command, []string) error { 25 | return func(c *cobra.Command, args []string) error { 26 | if cmdWithArgs, ok := cmd.(CommandWithArgs); ok { 27 | if err := cmdWithArgs.Validate(args); err != nil { 28 | return err 29 | } 30 | } 31 | return nil 32 | } 33 | } 34 | 35 | func mapDirectory(dir string, operation func(string) error) error { 36 | return filepath.Walk(dir, 37 | func(path string, info os.FileInfo, err error) error { 38 | if err != nil { 39 | return err 40 | } 41 | if info.Name() == "vendor" { 42 | return filepath.SkipDir 43 | } 44 | 45 | if info.IsDir() { 46 | return operation(path) 47 | } 48 | return nil 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /cmd/revert.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/DimitarPetrov/printracer/tracing" 6 | "github.com/spf13/cobra" 7 | "os" 8 | ) 9 | 10 | type RevertCmd struct { 11 | deinstrumenter tracing.CodeDeinstrumenter 12 | importsGroomer tracing.ImportsGroomer 13 | } 14 | 15 | func NewRevertCmd(deinstrumenter tracing.CodeDeinstrumenter, importsGroomer tracing.ImportsGroomer) *RevertCmd { 16 | return &RevertCmd{ 17 | deinstrumenter: deinstrumenter, 18 | importsGroomer: importsGroomer, 19 | } 20 | } 21 | 22 | func (rc *RevertCmd) Prepare() *cobra.Command { 23 | return &cobra.Command{ 24 | Use: "revert", 25 | Aliases: []string{"r"}, 26 | Short: "Reverts previously instrumented directory of go files", 27 | PreRunE: commonPreRunE(rc), 28 | RunE: commonRunE(rc), 29 | SilenceUsage: true, 30 | } 31 | } 32 | 33 | func (rc *RevertCmd) Run() error { 34 | wd, err := os.Getwd() 35 | if err != nil { 36 | return fmt.Errorf("error getting current working directory: %v", err) 37 | } 38 | 39 | return mapDirectory(wd, func(path string) error { 40 | err := rc.deinstrumenter.DeinstrumentDirectory(path) 41 | if err != nil { 42 | return err 43 | } 44 | return rc.importsGroomer.RemoveUnusedImportFromDirectory(path, map[string]string{"fmt": "", "runtime": "rt", "crypto/rand": ""}) // TODO: flag for import aliases 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /cmd/revert_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "github.com/DimitarPetrov/printracer/tracing/tracingfakes" 6 | "testing" 7 | ) 8 | 9 | func TestRevertCmd(t *testing.T) { 10 | fakeDeinstrumenter := &tracingfakes.FakeCodeDeinstrumenter{} 11 | fakeImportsGroomer := &tracingfakes.FakeImportsGroomer{} 12 | cmd := NewRevertCmd(fakeDeinstrumenter, fakeImportsGroomer).Prepare() 13 | 14 | if err := cmd.Execute(); err != nil { 15 | t.Fatal(err) 16 | } 17 | } 18 | 19 | func TestRevertCmdReturnsErrorWhenDeinstrumenterReturnError(t *testing.T) { 20 | fakeDeinstrumenter := &tracingfakes.FakeCodeDeinstrumenter{} 21 | fakeImportsGroomer := &tracingfakes.FakeImportsGroomer{} 22 | cmd := NewRevertCmd(fakeDeinstrumenter, fakeImportsGroomer).Prepare() 23 | 24 | expectedErr := errors.New("error") 25 | fakeDeinstrumenter.DeinstrumentDirectoryReturns(expectedErr) 26 | 27 | if err := cmd.Execute(); err != expectedErr { 28 | t.Error("Assertion failed!") 29 | } 30 | } 31 | 32 | func TestRevertCmdReturnsErrorWhenImportsGroomerReturnError(t *testing.T) { 33 | fakeDeinstrumenter := &tracingfakes.FakeCodeDeinstrumenter{} 34 | fakeImportsGroomer := &tracingfakes.FakeImportsGroomer{} 35 | cmd := NewRevertCmd(fakeDeinstrumenter, fakeImportsGroomer).Prepare() 36 | 37 | expectedErr := errors.New("error") 38 | fakeImportsGroomer.RemoveUnusedImportFromDirectoryReturns(expectedErr) 39 | 40 | if err := cmd.Execute(); err != expectedErr { 41 | t.Error("Assertion failed!") 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/DimitarPetrov/printracer/parser" 5 | "github.com/DimitarPetrov/printracer/tracing" 6 | "github.com/DimitarPetrov/printracer/vis" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | type RootCmd struct { 11 | instrumenter tracing.CodeInstrumenter 12 | deinstrumenter tracing.CodeDeinstrumenter 13 | importsGroomer tracing.ImportsGroomer 14 | parser parser.Parser 15 | visualizer vis.Visualizer 16 | } 17 | 18 | func NewRootCmd() *RootCmd { 19 | return &RootCmd{ 20 | instrumenter: tracing.NewCodeInstrumenter(), 21 | deinstrumenter: tracing.NewCodeDeinstrumenter(), 22 | importsGroomer: tracing.NewImportsGroomer(), 23 | parser: parser.NewParser(), 24 | visualizer: vis.NewVisualizer(), 25 | } 26 | } 27 | 28 | func (rc *RootCmd) Prepare() *cobra.Command { 29 | rootCmd := &cobra.Command{ 30 | Use: "printracer", 31 | Short: "Printracer CLI", 32 | Long: `printracer instruments every go file in the current working directory to print every function execution along with its arguments.`, 33 | } 34 | 35 | rootCmd.AddCommand(NewApplyCmd(rc.instrumenter, rc.importsGroomer).Prepare()) 36 | rootCmd.AddCommand(NewRevertCmd(rc.deinstrumenter, rc.importsGroomer).Prepare()) 37 | rootCmd.AddCommand(NewVisualizeCmd(rc.parser, rc.visualizer).Prepare()) 38 | 39 | return rootCmd 40 | } 41 | 42 | func (rc *RootCmd) Run() error { 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /cmd/root_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestRootCmd(t *testing.T) { 8 | cmd := NewRootCmd().Prepare() 9 | if err := cmd.Execute(); err != nil { 10 | t.Fatal(err) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /cmd/visualize.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/DimitarPetrov/printracer/parser" 6 | "github.com/DimitarPetrov/printracer/vis" 7 | "github.com/spf13/cobra" 8 | "io" 9 | "math" 10 | "os" 11 | ) 12 | 13 | type VisualizeCmd struct { 14 | parser parser.Parser 15 | visualizer vis.Visualizer 16 | 17 | input io.Reader 18 | 19 | outputFile string 20 | maxDepth int 21 | startingFunc string 22 | } 23 | 24 | func NewVisualizeCmd(parser parser.Parser, visualizer vis.Visualizer) *VisualizeCmd { 25 | return &VisualizeCmd{ 26 | parser: parser, 27 | visualizer: visualizer, 28 | } 29 | } 30 | 31 | func (vc *VisualizeCmd) Prepare() *cobra.Command { 32 | result := &cobra.Command{ 33 | Use: "visualize", 34 | Aliases: []string{"v"}, 35 | Short: "Generates html sequence diagram of a given trace (file with output of already instrumented code).", 36 | PreRunE: commonPreRunE(vc), 37 | RunE: commonRunE(vc), 38 | SilenceUsage: true, 39 | } 40 | 41 | result.Flags().StringVarP(&vc.outputFile, "output", "o", "calls", "name of the resulting html file when visualizing") 42 | result.Flags().IntVarP(&vc.maxDepth, "depth", "d", math.MaxInt32, "maximum depth in call graph. NOTE: If used visualization will be linear following the call stack of the starting func. Calls from different Goroutines will be ignored!") 43 | result.Flags().StringVarP(&vc.startingFunc, "func", "f", "", "name of the starting function in the visualization (the root of the diagram). NOTE: If used visualization will be linear following the call stack of the starting func. Calls from different Goroutines will be ignored!") 44 | return result 45 | } 46 | 47 | func (vc *VisualizeCmd) Validate(args []string) error { 48 | vc.input = os.Stdin 49 | if len(args) > 0 { 50 | f, err := os.Open(args[0]) 51 | if err != nil { 52 | return fmt.Errorf("error opening input file %s: %v", args[0], err) 53 | } 54 | vc.input = f 55 | } 56 | return nil 57 | } 58 | 59 | func (vc *VisualizeCmd) Run() error { 60 | events, err := vc.parser.Parse(vc.input) 61 | if err != nil { 62 | return fmt.Errorf("error while parsing input: %v", err) 63 | } 64 | if err := vc.visualizer.Visualize(events, vc.maxDepth, vc.startingFunc, vc.outputFile); err != nil { 65 | return fmt.Errorf("error visualizing sequence diagram: %v", err) 66 | } 67 | return nil 68 | } 69 | -------------------------------------------------------------------------------- /cmd/visualize_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "github.com/DimitarPetrov/printracer/parser/parserfakes" 6 | "github.com/DimitarPetrov/printracer/vis/visfakes" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func TestVisualizeCmd(t *testing.T) { 12 | fakeVisualizer := &visfakes.FakeVisualizer{} 13 | fakeParser := &parserfakes.FakeParser{} 14 | cmd := NewVisualizeCmd(fakeParser, fakeVisualizer).Prepare() 15 | 16 | if err := cmd.Execute(); err != nil { 17 | t.Fatal(err) 18 | } 19 | } 20 | 21 | func TestVisualizeCmdReturnsErrorWhenParserReturnError(t *testing.T) { 22 | fakeVisualizer := &visfakes.FakeVisualizer{} 23 | fakeParser := &parserfakes.FakeParser{} 24 | cmd := NewVisualizeCmd(fakeParser, fakeVisualizer).Prepare() 25 | 26 | expectedErr := errors.New("error") 27 | fakeParser.ParseReturns(nil, expectedErr) 28 | 29 | if err := cmd.Execute(); err == nil { 30 | t.Error("Expected error to have occured!") 31 | } else if !strings.Contains(err.Error(), expectedErr.Error()) { 32 | t.Error("Assertion failed!") 33 | } 34 | } 35 | 36 | func TestVisualizeCmdReturnsErrorWhenVisualizerReturnError(t *testing.T) { 37 | fakeVisualizer := &visfakes.FakeVisualizer{} 38 | fakeParser := &parserfakes.FakeParser{} 39 | cmd := NewVisualizeCmd(fakeParser, fakeVisualizer).Prepare() 40 | 41 | expectedErr := errors.New("error") 42 | fakeVisualizer.VisualizeReturns(expectedErr) 43 | 44 | if err := cmd.Execute(); err == nil { 45 | t.Error("Expected error to have occured!") 46 | } else if !strings.Contains(err.Error(), expectedErr.Error()) { 47 | t.Error("Assertion failed!") 48 | } 49 | } 50 | 51 | func TestVisualizeCmdErrorWhileValidatingArgs(t *testing.T) { 52 | fakeVisualizer := &visfakes.FakeVisualizer{} 53 | fakeParser := &parserfakes.FakeParser{} 54 | cmd := NewVisualizeCmd(fakeParser, fakeVisualizer) 55 | 56 | if err := cmd.Validate([]string{"test"}); err == nil { 57 | t.Error("Expected error to have occured!") 58 | } else if !strings.Contains(err.Error(), "error opening input file") { 59 | t.Error("Assertion failed!") 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /examples/example1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DimitarPetrov/printracer/df97a690c08bdb9d7523524355ecff0e8a385531/examples/example1.png -------------------------------------------------------------------------------- /examples/example2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DimitarPetrov/printracer/df97a690c08bdb9d7523524355ecff0e8a385531/examples/example2.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/DimitarPetrov/printracer 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/dave/dst v0.26.0 7 | github.com/spf13/cobra v1.0.0 8 | github.com/stretchr/testify v1.6.1 // indirect 9 | golang.org/x/tools v0.0.0-20200822203824-307de81be3f4 10 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 4 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 5 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 6 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 7 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 8 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 9 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 10 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 11 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 12 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 13 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 14 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 15 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 16 | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 17 | github.com/dave/dst v0.26.0 h1:Iewsg5Km+62a9ruBow1wEkQFbTpTnAO/6CM6CAVzTgw= 18 | github.com/dave/dst v0.26.0/go.mod h1:UMDJuIRPfyUCC78eFuB+SV/WI8oDeyFDvM/JR6NI3IU= 19 | github.com/dave/gopackages v0.0.0-20170318123100-46e7023ec56e/go.mod h1:i00+b/gKdIDIxuLDFob7ustLAVqhsZRk2qVZrArELGQ= 20 | github.com/dave/jennifer v1.2.0/go.mod h1:fIb+770HOpJ2fmN9EPPKOqm1vMGhB+TwXKMZhrIygKg= 21 | github.com/dave/kerr v0.0.0-20170318121727-bc25dd6abe8e/go.mod h1:qZqlPyPvfsDJt+3wHJ1EvSXDuVjFTK0j2p/ca+gtsb8= 22 | github.com/dave/rebecca v0.9.1/go.mod h1:N6XYdMD/OKw3lkF3ywh8Z6wPGuwNFDNtWYEMFWEmXBA= 23 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 24 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 25 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 26 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 27 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 28 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= 29 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 30 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 31 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 32 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 33 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 34 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 35 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 36 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 37 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 38 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 39 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 40 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 41 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 42 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 43 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 44 | github.com/google/pprof v0.0.0-20181127221834-b4f47329b966/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 45 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 46 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 47 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 48 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 49 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 50 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 51 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 52 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 53 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 54 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 55 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 56 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 57 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 58 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 59 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 60 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 61 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 62 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 63 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 64 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 65 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 66 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 67 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 68 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 69 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 70 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 71 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 72 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 73 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 74 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 75 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= 76 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 77 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 78 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 79 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 80 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 81 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 82 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= 83 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 84 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 85 | github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= 86 | github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 87 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 88 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 89 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 90 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 91 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 92 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 93 | github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8= 94 | github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= 95 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 96 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 97 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 98 | github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= 99 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 100 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 101 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 102 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 103 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 104 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 105 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= 106 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 107 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 108 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 109 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 110 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 111 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 112 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 113 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 114 | golang.org/x/arch v0.0.0-20180920145803-b19384d3c130/go.mod h1:cYlCBUl1MsqxdiKgmc4uh7TxZfWSFLOGSRR090WDxt8= 115 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 116 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 117 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 118 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 119 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 120 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 121 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 122 | golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= 123 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 124 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 125 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 126 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 127 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 128 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 129 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 130 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 131 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 132 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 133 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 134 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 135 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 136 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 137 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 138 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 139 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 140 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 141 | golang.org/x/sys v0.0.0-20180903190138-2b024373dcd9/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 142 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 143 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 144 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 145 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 146 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 147 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 148 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 149 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 150 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 151 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 152 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 153 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 154 | golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 155 | golang.org/x/tools v0.0.0-20200822203824-307de81be3f4 h1:r0nbB2EeRbGpnVeqxlkgiBpNi/bednpSg78qzZGOuv0= 156 | golang.org/x/tools v0.0.0-20200822203824-307de81be3f4/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 157 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 158 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 159 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 160 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 161 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 162 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 163 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 164 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 165 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 166 | google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 167 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 168 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 169 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 170 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 171 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 172 | gopkg.in/src-d/go-billy.v4 v4.3.0 h1:KtlZ4c1OWbIs4jCv5ZXrTqG8EQocr0g/d4DjNg70aek= 173 | gopkg.in/src-d/go-billy.v4 v4.3.0/go.mod h1:tm33zBoOwxjYHZIE+OV8bxTWFMJLrconzFMd38aARFk= 174 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 175 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 176 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 177 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 178 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 179 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 180 | -------------------------------------------------------------------------------- /parser/parser.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "bufio" 5 | "io" 6 | "strings" 7 | ) 8 | 9 | //go:generate counterfeiter . Parser 10 | type Parser interface { 11 | Parse(in io.Reader) ([]FuncEvent, error) 12 | } 13 | 14 | type FuncEventType int 15 | 16 | const ( 17 | Invocation FuncEventType = iota 18 | Returning 19 | ) 20 | 21 | type FuncEvent interface { 22 | GetCaller() string 23 | GetCallee() string 24 | GetCallID() string 25 | } 26 | 27 | type InvocationEvent struct { 28 | Caller string 29 | Callee string 30 | CallID string 31 | Args string 32 | } 33 | 34 | func (ie *InvocationEvent) GetCaller() string { 35 | return ie.Caller 36 | } 37 | 38 | func (ie *InvocationEvent) GetCallee() string { 39 | return ie.Callee 40 | } 41 | 42 | func (ie *InvocationEvent) GetCallID() string { 43 | return ie.CallID 44 | } 45 | 46 | type ReturningEvent struct { 47 | Caller string 48 | Callee string 49 | CallID string 50 | } 51 | 52 | func (re *ReturningEvent) GetCaller() string { 53 | return re.Caller 54 | } 55 | 56 | func (re *ReturningEvent) GetCallee() string { 57 | return re.Callee 58 | } 59 | 60 | func (re *ReturningEvent) GetCallID() string { 61 | return re.CallID 62 | } 63 | 64 | type parser struct { 65 | } 66 | 67 | func NewParser() Parser { 68 | return &parser{} 69 | } 70 | 71 | func (p *parser) Parse(in io.Reader) ([]FuncEvent, error) { 72 | var events []FuncEvent 73 | scanner := bufio.NewScanner(in) 74 | for scanner.Scan() { 75 | row := scanner.Text() 76 | lastSemicolon := strings.LastIndex(row, ";") 77 | msg := row[:lastSemicolon] 78 | secondHalf := row[lastSemicolon+1:] 79 | callID := strings.Split(secondHalf, "=")[1] 80 | if strings.HasPrefix(msg, "Entering function") { 81 | words := strings.Split(msg, " ") 82 | events = append(events, &InvocationEvent{ 83 | Callee: normalizeFuncName(words[2]), 84 | Caller: normalizeFuncName(words[5]), 85 | Args: strings.Join(words[6:], " "), 86 | CallID: callID, 87 | }) 88 | } 89 | 90 | if strings.HasPrefix(msg, "Exiting function") { 91 | words := strings.Split(msg, " ") 92 | events = append(events, &ReturningEvent{ 93 | Callee: normalizeFuncName(words[2]), 94 | Caller: normalizeFuncName(words[5]), 95 | CallID: callID, 96 | }) 97 | } 98 | } 99 | if err := scanner.Err(); err != nil { 100 | return nil, err 101 | } 102 | 103 | return events, nil 104 | } 105 | 106 | func normalizeFuncName(funcName string) string { 107 | return funcName[strings.LastIndex(funcName, "/")+1:] 108 | } 109 | -------------------------------------------------------------------------------- /parser/parser_test.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "bytes" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestParser_Parse(t *testing.T) { 10 | input := `Entering function main.main called by runtime.main; callID=1d8ca74e-c860-8a75-fc36-fe6d34350f0c 11 | Entering function main.foo called by main.main with args (5) (false); callID=973355a9-2ec6-095c-9137-7a1081ac0a5f 12 | Entering function main.bar called by main.foo with args (test string); callID=6c294dfd-4c6a-39b1-474e-314bee73f514 13 | Entering function main.baz called by main.bar; callID=a019a297-0a6e-a792-0e3f-23c33a44622f 14 | Exiting function main.baz called by main.bar; callID=a019a297-0a6e-a792-0e3f-23c33a44622f 15 | Exiting function main.bar called by main.foo; callID=6c294dfd-4c6a-39b1-474e-314bee73f514 16 | Exiting function main.foo called by main.main; callID=973355a9-2ec6-095c-9137-7a1081ac0a5f 17 | Exiting function main.main called by runtime.main; callID=1d8ca74e-c860-8a75-fc36-fe6d34350f0c` 18 | 19 | expected := []FuncEvent{ 20 | &InvocationEvent{ 21 | Caller: "runtime.main", 22 | Callee: "main.main", 23 | CallID: "1d8ca74e-c860-8a75-fc36-fe6d34350f0c", 24 | }, 25 | &InvocationEvent{ 26 | Caller: "main.main", 27 | Callee: "main.foo", 28 | CallID: "973355a9-2ec6-095c-9137-7a1081ac0a5f", 29 | Args: "with args (5) (false)", 30 | }, 31 | &InvocationEvent{ 32 | Caller: "main.foo", 33 | Callee: "main.bar", 34 | CallID: "6c294dfd-4c6a-39b1-474e-314bee73f514", 35 | Args: "with args (test string)", 36 | }, 37 | &InvocationEvent{ 38 | Caller: "main.bar", 39 | Callee: "main.baz", 40 | CallID: "a019a297-0a6e-a792-0e3f-23c33a44622f", 41 | }, 42 | &ReturningEvent{ 43 | Caller: "main.bar", 44 | Callee: "main.baz", 45 | CallID: "a019a297-0a6e-a792-0e3f-23c33a44622f", 46 | }, 47 | &ReturningEvent{ 48 | Caller: "main.foo", 49 | Callee: "main.bar", 50 | CallID: "6c294dfd-4c6a-39b1-474e-314bee73f514", 51 | }, 52 | &ReturningEvent{ 53 | Caller: "main.main", 54 | Callee: "main.foo", 55 | CallID: "973355a9-2ec6-095c-9137-7a1081ac0a5f", 56 | }, 57 | &ReturningEvent{ 58 | Caller: "runtime.main", 59 | Callee: "main.main", 60 | CallID: "1d8ca74e-c860-8a75-fc36-fe6d34350f0c", 61 | }, 62 | } 63 | 64 | actual, err := NewParser().Parse(bytes.NewBufferString(input)) 65 | if err != nil { 66 | t.Fatal(err) 67 | } 68 | 69 | if !reflect.DeepEqual(expected, actual) { 70 | t.Error("Assertion Failed!") 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /parser/parserfakes/fake_parser.go: -------------------------------------------------------------------------------- 1 | // Code generated by counterfeiter. DO NOT EDIT. 2 | package parserfakes 3 | 4 | import ( 5 | "io" 6 | "sync" 7 | 8 | "github.com/DimitarPetrov/printracer/parser" 9 | ) 10 | 11 | type FakeParser struct { 12 | ParseStub func(io.Reader) ([]parser.FuncEvent, error) 13 | parseMutex sync.RWMutex 14 | parseArgsForCall []struct { 15 | arg1 io.Reader 16 | } 17 | parseReturns struct { 18 | result1 []parser.FuncEvent 19 | result2 error 20 | } 21 | parseReturnsOnCall map[int]struct { 22 | result1 []parser.FuncEvent 23 | result2 error 24 | } 25 | invocations map[string][][]interface{} 26 | invocationsMutex sync.RWMutex 27 | } 28 | 29 | func (fake *FakeParser) Parse(arg1 io.Reader) ([]parser.FuncEvent, error) { 30 | fake.parseMutex.Lock() 31 | ret, specificReturn := fake.parseReturnsOnCall[len(fake.parseArgsForCall)] 32 | fake.parseArgsForCall = append(fake.parseArgsForCall, struct { 33 | arg1 io.Reader 34 | }{arg1}) 35 | fake.recordInvocation("Parse", []interface{}{arg1}) 36 | fake.parseMutex.Unlock() 37 | if fake.ParseStub != nil { 38 | return fake.ParseStub(arg1) 39 | } 40 | if specificReturn { 41 | return ret.result1, ret.result2 42 | } 43 | fakeReturns := fake.parseReturns 44 | return fakeReturns.result1, fakeReturns.result2 45 | } 46 | 47 | func (fake *FakeParser) ParseCallCount() int { 48 | fake.parseMutex.RLock() 49 | defer fake.parseMutex.RUnlock() 50 | return len(fake.parseArgsForCall) 51 | } 52 | 53 | func (fake *FakeParser) ParseCalls(stub func(io.Reader) ([]parser.FuncEvent, error)) { 54 | fake.parseMutex.Lock() 55 | defer fake.parseMutex.Unlock() 56 | fake.ParseStub = stub 57 | } 58 | 59 | func (fake *FakeParser) ParseArgsForCall(i int) io.Reader { 60 | fake.parseMutex.RLock() 61 | defer fake.parseMutex.RUnlock() 62 | argsForCall := fake.parseArgsForCall[i] 63 | return argsForCall.arg1 64 | } 65 | 66 | func (fake *FakeParser) ParseReturns(result1 []parser.FuncEvent, result2 error) { 67 | fake.parseMutex.Lock() 68 | defer fake.parseMutex.Unlock() 69 | fake.ParseStub = nil 70 | fake.parseReturns = struct { 71 | result1 []parser.FuncEvent 72 | result2 error 73 | }{result1, result2} 74 | } 75 | 76 | func (fake *FakeParser) ParseReturnsOnCall(i int, result1 []parser.FuncEvent, result2 error) { 77 | fake.parseMutex.Lock() 78 | defer fake.parseMutex.Unlock() 79 | fake.ParseStub = nil 80 | if fake.parseReturnsOnCall == nil { 81 | fake.parseReturnsOnCall = make(map[int]struct { 82 | result1 []parser.FuncEvent 83 | result2 error 84 | }) 85 | } 86 | fake.parseReturnsOnCall[i] = struct { 87 | result1 []parser.FuncEvent 88 | result2 error 89 | }{result1, result2} 90 | } 91 | 92 | func (fake *FakeParser) Invocations() map[string][][]interface{} { 93 | fake.invocationsMutex.RLock() 94 | defer fake.invocationsMutex.RUnlock() 95 | fake.parseMutex.RLock() 96 | defer fake.parseMutex.RUnlock() 97 | copiedInvocations := map[string][][]interface{}{} 98 | for key, value := range fake.invocations { 99 | copiedInvocations[key] = value 100 | } 101 | return copiedInvocations 102 | } 103 | 104 | func (fake *FakeParser) recordInvocation(key string, args []interface{}) { 105 | fake.invocationsMutex.Lock() 106 | defer fake.invocationsMutex.Unlock() 107 | if fake.invocations == nil { 108 | fake.invocations = map[string][][]interface{}{} 109 | } 110 | if fake.invocations[key] == nil { 111 | fake.invocations[key] = [][]interface{}{} 112 | } 113 | fake.invocations[key] = append(fake.invocations[key], args) 114 | } 115 | 116 | var _ parser.Parser = new(FakeParser) 117 | -------------------------------------------------------------------------------- /printracer.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/DimitarPetrov/printracer/cmd" 5 | "log" 6 | ) 7 | 8 | func main() { 9 | if err := cmd.NewRootCmd().Prepare().Execute(); err != nil { 10 | log.Fatal(err) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tracing/deinstrument.go: -------------------------------------------------------------------------------- 1 | package tracing 2 | 3 | import ( 4 | "fmt" 5 | "github.com/dave/dst" 6 | "github.com/dave/dst/decorator" 7 | "go/ast" 8 | "go/parser" 9 | "go/token" 10 | "io" 11 | "os" 12 | "reflect" 13 | ) 14 | 15 | type codeDeinstrumenter struct { 16 | } 17 | 18 | func NewCodeDeinstrumenter() CodeDeinstrumenter { 19 | return &codeDeinstrumenter{} 20 | } 21 | 22 | func (cd *codeDeinstrumenter) DeinstrumentDirectory(path string) error { 23 | fset := token.NewFileSet() 24 | filter := func(info os.FileInfo) bool { 25 | return testsFilter(info) && generatedFilter(path, info) 26 | } 27 | pkgs, err := parser.ParseDir(fset, path, filter, parser.ParseComments) 28 | if err != nil { 29 | return fmt.Errorf("failed parsing go files in directory %s: %v", path, err) 30 | } 31 | 32 | for _, pkg := range pkgs { 33 | if err := cd.DeinstrumentPackage(fset, pkg); err != nil { 34 | return err 35 | } 36 | } 37 | return nil 38 | } 39 | 40 | func (cd *codeDeinstrumenter) DeinstrumentPackage(fset *token.FileSet, pkg *ast.Package) error { 41 | for fileName, file := range pkg.Files { 42 | sourceFile, err := os.OpenFile(fileName, os.O_TRUNC|os.O_WRONLY, 0664) 43 | if err != nil { 44 | return fmt.Errorf("failed opening file %s: %v", fileName, err) 45 | } 46 | if err := cd.DeinstrumentFile(fset, file, sourceFile); err != nil { 47 | return fmt.Errorf("failed deinstrumenting file %s: %v", fileName, err) 48 | } 49 | } 50 | return nil 51 | } 52 | 53 | func (cd *codeDeinstrumenter) DeinstrumentFile(fset *token.FileSet, file *ast.File, out io.Writer) error { 54 | // Needed because ast does not support floating comments and deletes them. 55 | // In order to preserve all comments we just pre-parse it to dst which treats them as first class citizens. 56 | f, err := decorator.DecorateFile(fset, file) 57 | if err != nil { 58 | return fmt.Errorf("failed converting file from ast to dst: %v", err) 59 | } 60 | dst.Inspect(f, func(n dst.Node) bool { 61 | switch t := n.(type) { 62 | case *dst.FuncDecl: 63 | if len(t.Body.List) >= instrumentationStmtsCount { 64 | firstStmntDecorations := t.Body.List[0].Decorations().Start.All() 65 | secondStmntDecorations := t.Body.List[instrumentationStmtsCount-1].Decorations().End.All() 66 | if len(firstStmntDecorations) > 0 && firstStmntDecorations[0] == printracerCommentWatermark && 67 | len(secondStmntDecorations) > 0 && secondStmntDecorations[0] == printracerCommentWatermark { 68 | 69 | if checkInstrumentationStatementsIntegrity(t) { 70 | t.Body.List = t.Body.List[instrumentationStmtsCount:] 71 | if len(t.Body.List) > 0 { 72 | t.Body.List[0].Decorations().Before = dst.None 73 | } 74 | } 75 | } 76 | } 77 | } 78 | return true 79 | }) 80 | 81 | return decorator.Fprint(out, f) 82 | } 83 | 84 | func checkInstrumentationStatementsIntegrity(f *dst.FuncDecl) bool { 85 | stmts := f.Body.List 86 | instrumentationStmts := buildInstrumentationStmts(f) 87 | 88 | for i := 0; i < instrumentationStmtsCount; i++ { 89 | if !equalStmt(stmts[i], instrumentationStmts[i]) { 90 | return false 91 | } 92 | } 93 | return true 94 | } 95 | 96 | func equalStmt(stmt1, stmt2 dst.Stmt) bool { 97 | switch t := stmt1.(type) { 98 | case *dst.AssignStmt: 99 | instStmt, ok := stmt2.(*dst.AssignStmt) 100 | if !ok { 101 | return false 102 | } 103 | if !(equalExprSlice(t.Lhs, instStmt.Lhs) && equalExprSlice(t.Rhs, instStmt.Rhs) && reflect.DeepEqual(t.Tok, instStmt.Tok)) { 104 | return false 105 | } 106 | return true 107 | case *dst.IfStmt: 108 | instStmt, ok := stmt2.(*dst.IfStmt) 109 | if !ok { 110 | return false 111 | } 112 | if !(equalStmt(t.Init, instStmt.Init) && equalExpr(t.Cond, instStmt.Cond) && equalStmt(t.Body, instStmt.Body) && equalStmt(t.Else, instStmt.Else)) { 113 | return false 114 | } 115 | return true 116 | case *dst.ExprStmt: 117 | instStmt, ok := stmt2.(*dst.ExprStmt) 118 | if !ok { 119 | return false 120 | } 121 | if !(equalExpr(t.X, instStmt.X)) { 122 | return false 123 | } 124 | return true 125 | case *dst.DeferStmt: 126 | instStmt, ok := stmt2.(*dst.DeferStmt) 127 | if !ok { 128 | return false 129 | } 130 | if !(equalExpr(t.Call, instStmt.Call)) { 131 | return false 132 | } 133 | return true 134 | case *dst.BlockStmt: 135 | instStmt, ok := stmt2.(*dst.BlockStmt) 136 | if !ok { 137 | return false 138 | } 139 | if len(t.List) != len(instStmt.List) || t.RbraceHasNoPos != instStmt.RbraceHasNoPos { 140 | return false 141 | } 142 | for i, stmt1 := range t.List { 143 | if !equalStmt(stmt1, instStmt.List[i]) { 144 | return false 145 | } 146 | } 147 | return true 148 | } 149 | return reflect.DeepEqual(stmt1, stmt2) 150 | } 151 | 152 | func equalExprSlice(exprSlice1, exprSlice2 []dst.Expr) bool { 153 | if len(exprSlice1) != len(exprSlice2) { 154 | return false 155 | } 156 | for i, expr1 := range exprSlice1 { 157 | if !equalExpr(expr1, exprSlice2[i]) { 158 | return false 159 | } 160 | } 161 | return true 162 | } 163 | 164 | func equalExpr(expr1, expr2 dst.Expr) bool { 165 | switch t := expr1.(type) { 166 | case *dst.Ident: 167 | instExpr, ok := expr2.(*dst.Ident) 168 | if !ok { 169 | instExpr, ok := expr2.(*dst.BasicLit) 170 | if !ok { 171 | return false 172 | } 173 | return t.Name == instExpr.Value 174 | } 175 | return t.Name == instExpr.Name && t.Path == instExpr.Path 176 | case *dst.CallExpr: 177 | instExpr, ok := expr2.(*dst.CallExpr) 178 | if !ok { 179 | return false 180 | } 181 | if !(equalExprSlice(t.Args, instExpr.Args) && equalExpr(t.Fun, instExpr.Fun)) { 182 | return false 183 | } 184 | return true 185 | case *dst.SelectorExpr: 186 | instExpr, ok := expr2.(*dst.SelectorExpr) 187 | if !ok { 188 | return false 189 | } 190 | if !(equalExpr(t.X, instExpr.X) && equalExpr(t.Sel, instExpr.Sel)) { 191 | return false 192 | } 193 | return true 194 | case *dst.SliceExpr: 195 | instExpr, ok := expr2.(*dst.SliceExpr) 196 | if !ok { 197 | return false 198 | } 199 | if !(t.Slice3 == instExpr.Slice3 && equalExpr(t.X, instExpr.X) && equalExpr(t.High, instExpr.High) && equalExpr(t.Low, instExpr.Low) && equalExpr(t.Max, instExpr.Max)) { 200 | return false 201 | } 202 | return true 203 | case *dst.BasicLit: 204 | instExpr, ok := expr2.(*dst.BasicLit) 205 | if !ok { 206 | return false 207 | } 208 | return t.Value == instExpr.Value && t.Kind == instExpr.Kind 209 | } 210 | return reflect.DeepEqual(expr1, expr2) 211 | } 212 | -------------------------------------------------------------------------------- /tracing/deinstrument_test.go: -------------------------------------------------------------------------------- 1 | package tracing 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "go/parser" 7 | "go/token" 8 | "io/ioutil" 9 | "os" 10 | "testing" 11 | ) 12 | 13 | func TestDeinstrumentFile(t *testing.T) { 14 | tests := []struct { 15 | Name string 16 | InputCode string 17 | OutputCode string 18 | }{ 19 | {Name: "DeinstrumentFileWithoutImports", InputCode: resultCodeWithoutImports, OutputCode: codeWithoutImports}, 20 | {Name: "DeinstrumentFileWithFmtImportOnly", InputCode: resultCodeWithFmtImport, OutputCode: codeWithFmtImport}, 21 | {Name: "DeinstrumentFileWithMultipleImports", InputCode: resultCodeWithMultipleImports, OutputCode: codeWithMultipleImports}, 22 | {Name: "DeinstrumentFileWithoutFmtImport", InputCode: resultCodeWithImportsWithoutFmt, OutputCode: codeWithImportsWithoutFmt}, 23 | {Name: "DeinstrumentFileWithoutFunctions", InputCode: resultCodeWithoutFunction, OutputCode: codeWithoutFunction}, 24 | {Name: "DeinstrumentFileWithoutPreviousInstrumentation", InputCode: codeWithMultipleImports, OutputCode: codeWithMultipleImports}, 25 | {Name: "DeinstrumentFileDoesNotChangeManuallyEditedFunctions", InputCode: editedResultCodeWithoutImports, OutputCode: editedResultCodeWithoutImports}, 26 | } 27 | 28 | for _, test := range tests { 29 | t.Run(test.Name, func(t *testing.T) { 30 | fset := token.NewFileSet() 31 | file, err := parser.ParseFile(fset, "", test.InputCode, parser.ParseComments) 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | var buff bytes.Buffer 36 | if err := NewCodeDeinstrumenter().DeinstrumentFile(fset, file, &buff); err != nil { 37 | t.Fatal(err) 38 | } 39 | 40 | file, err = parser.ParseFile(fset, "", buff.String(), parser.ParseComments) 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | 45 | var buff2 bytes.Buffer 46 | if err := NewImportsGroomer().RemoveUnusedImportFromFile(fset, file, &buff2, map[string]string{"fmt": "", "runtime": "rt", "crypto/rand": ""}); err != nil { 47 | t.Fatal(err) 48 | } 49 | 50 | if buff2.String() != test.OutputCode { 51 | t.Errorf("Assertion failed! Expected %s god %s", test.OutputCode, buff2.String()) 52 | } 53 | }) 54 | } 55 | } 56 | 57 | func TestDeinstrumentDirectory(t *testing.T) { 58 | if err := os.Mkdir("test", 0777); err != nil { 59 | t.Fatal(err) 60 | } 61 | defer func() { 62 | if err := os.RemoveAll("test"); err != nil { 63 | t.Fatal(err) 64 | } 65 | }() 66 | 67 | filePairs := []struct { 68 | InputCode string 69 | OutputCode string 70 | }{ 71 | {InputCode: resultCodeWithoutImports, OutputCode: codeWithoutImports}, 72 | {InputCode: resultCodeWithFmtImport, OutputCode: codeWithFmtImport}, 73 | {InputCode: resultCodeWithMultipleImports, OutputCode: codeWithMultipleImports}, 74 | {InputCode: resultCodeWithImportsWithoutFmt, OutputCode: codeWithImportsWithoutFmt}, 75 | {InputCode: resultCodeWithoutFunction, OutputCode: codeWithoutFunction}, 76 | {InputCode: editedResultCodeWithoutImports, OutputCode: editedResultCodeWithoutImports}, 77 | } 78 | 79 | i := 0 80 | for _, filePair := range filePairs { 81 | if err := ioutil.WriteFile(fmt.Sprintf("test/test%d.go", i), []byte(filePair.InputCode), 0777); err != nil { 82 | t.Fatal(err) 83 | } 84 | i++ 85 | } 86 | 87 | if err := NewCodeDeinstrumenter().DeinstrumentDirectory("test"); err != nil { 88 | t.Fatal(err) 89 | } 90 | 91 | if err := NewImportsGroomer().RemoveUnusedImportFromDirectory("test", map[string]string{"fmt": "", "runtime": "rt", "crypto/rand": ""}); err != nil { 92 | t.Fatal(err) 93 | } 94 | 95 | i = 0 96 | for _, filePair := range filePairs { 97 | data, err := ioutil.ReadFile(fmt.Sprintf("test/test%d.go", i)) 98 | if err != nil { 99 | t.Fatal(err) 100 | } 101 | if string(data) != filePair.OutputCode { 102 | t.Error("Assertion failed!") 103 | } 104 | i++ 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /tracing/instrument.go: -------------------------------------------------------------------------------- 1 | package tracing 2 | 3 | import ( 4 | "fmt" 5 | "github.com/dave/dst" 6 | "github.com/dave/dst/decorator" 7 | "go/ast" 8 | "go/parser" 9 | "go/token" 10 | "golang.org/x/tools/go/ast/astutil" 11 | "io" 12 | "os" 13 | ) 14 | 15 | const funcNameVarName = "funcName" 16 | const funcPCVarName = "funcPC" 17 | 18 | const callerFuncNameVarName = "caller" 19 | const defaultCallerName = "unknown" 20 | const callerFuncPCVarName = "callerPC" 21 | 22 | const callIDVarName = "callID" 23 | 24 | const printracerCommentWatermark = "/* prinTracer */" 25 | 26 | const instrumentationStmtsCount = 9 // Acts like a contract of how many statements instrumentation adds and deinstrumentation removes. 27 | 28 | func buildInstrumentationStmts(f *dst.FuncDecl) [instrumentationStmtsCount]dst.Stmt { 29 | return [instrumentationStmtsCount]dst.Stmt{ 30 | newAssignStmt(funcNameVarName, f.Name.Name), 31 | newAssignStmt(callerFuncNameVarName, defaultCallerName), 32 | newGetFuncNameIfStatement("0", funcPCVarName, funcNameVarName), 33 | newGetFuncNameIfStatement("1", callerFuncPCVarName, callerFuncNameVarName), 34 | newMakeByteSliceStmt(), 35 | newRandReadStmt(), 36 | newParseUUIDFromByteSliceStmt(callIDVarName), 37 | &dst.ExprStmt{ 38 | X: newPrintExprWithArgs(buildEnteringFunctionArgs(f)), 39 | }, 40 | &dst.DeferStmt{ 41 | Call: newPrintExprWithArgs(buildExitFunctionArgs()), 42 | }, 43 | } 44 | } 45 | 46 | type codeInstrumenter struct { 47 | } 48 | 49 | func NewCodeInstrumenter() CodeInstrumenter { 50 | return &codeInstrumenter{} 51 | } 52 | 53 | func (ci *codeInstrumenter) InstrumentDirectory(path string) error { 54 | fset := token.NewFileSet() 55 | filter := func(info os.FileInfo) bool { 56 | return testsFilter(info) && generatedFilter(path, info) 57 | } 58 | pkgs, err := parser.ParseDir(fset, path, filter, parser.ParseComments) 59 | if err != nil { 60 | return fmt.Errorf("failed parsing go files in directory %s: %v", path, err) 61 | } 62 | 63 | for _, pkg := range pkgs { 64 | if err := ci.InstrumentPackage(fset, pkg); err != nil { 65 | return err 66 | } 67 | } 68 | return nil 69 | } 70 | 71 | func (ci *codeInstrumenter) InstrumentPackage(fset *token.FileSet, pkg *ast.Package) error { 72 | for fileName, file := range pkg.Files { 73 | sourceFile, err := os.OpenFile(fileName, os.O_TRUNC|os.O_WRONLY, 0664) 74 | if err != nil { 75 | return fmt.Errorf("failed opening file %s: %v", fileName, err) 76 | } 77 | if err := ci.InstrumentFile(fset, file, sourceFile); err != nil { 78 | return fmt.Errorf("failed instrumenting file %s: %v", fileName, err) 79 | } 80 | } 81 | return nil 82 | } 83 | 84 | func (ci *codeInstrumenter) InstrumentFile(fset *token.FileSet, file *ast.File, out io.Writer) error { 85 | astutil.AddImport(fset, file, "fmt") 86 | astutil.AddNamedImport(fset, file, "rt", "runtime") 87 | astutil.AddImport(fset, file, "crypto/rand") 88 | 89 | // Needed because ast does not support floating comments and deletes them. 90 | // In order to preserve all comments we just pre-parse it to dst which treats them as first class citizens. 91 | f, err := decorator.DecorateFile(fset, file) 92 | if err != nil { 93 | return fmt.Errorf("failed converting file from ast to dst: %v", err) 94 | } 95 | 96 | dst.Inspect(f, func(n dst.Node) bool { 97 | switch t := n.(type) { 98 | case *dst.FuncDecl: 99 | if !ci.hasInstrumentationWatermark(t) { 100 | instrumentationStmts := buildInstrumentationStmts(t) 101 | t.Body.List = append(instrumentationStmts[:], t.Body.List...) 102 | 103 | t.Body.List[0].Decorations().Before = dst.EmptyLine 104 | t.Body.List[0].Decorations().Start.Append(printracerCommentWatermark) 105 | t.Body.List[instrumentationStmtsCount-1].Decorations().After = dst.EmptyLine 106 | t.Body.List[instrumentationStmtsCount-1].Decorations().End.Append(printracerCommentWatermark) 107 | } 108 | } 109 | return true 110 | }) 111 | return decorator.Fprint(out, f) 112 | } 113 | 114 | func (ci *codeInstrumenter) hasInstrumentationWatermark(f *dst.FuncDecl) bool { 115 | if len(f.Body.List) > 0 { 116 | firstStmntDecorations := f.Body.List[0].Decorations().Start.All() 117 | if len(firstStmntDecorations) > 0 && firstStmntDecorations[0] == printracerCommentWatermark { 118 | return true 119 | } 120 | } 121 | return false 122 | } 123 | -------------------------------------------------------------------------------- /tracing/instrument_test.go: -------------------------------------------------------------------------------- 1 | package tracing 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "go/parser" 7 | "go/token" 8 | "io/ioutil" 9 | "os" 10 | "testing" 11 | ) 12 | 13 | const codeWithoutImports = `package a 14 | 15 | func test(i int, b bool) int { 16 | if b { 17 | return i 18 | } 19 | return 0 20 | } 21 | 22 | func main() { 23 | i := test(2, false) 24 | } 25 | ` 26 | 27 | const resultCodeWithoutImports = `package a 28 | 29 | import ( 30 | "crypto/rand" 31 | "fmt" 32 | rt "runtime" 33 | ) 34 | 35 | func test(i int, b bool) int { 36 | 37 | /* prinTracer */ 38 | funcName := "test" 39 | caller := "unknown" 40 | if funcPC, _, _, ok := rt.Caller(0); ok { 41 | funcName = rt.FuncForPC(funcPC).Name() 42 | } 43 | if callerPC, _, _, ok := rt.Caller(1); ok { 44 | caller = rt.FuncForPC(callerPC).Name() 45 | } 46 | idBytes := make([]byte, 16) 47 | _, _ = rand.Read(idBytes) 48 | callID := fmt.Sprintf("%x-%x-%x-%x-%x", idBytes[0:4], idBytes[4:6], idBytes[6:8], idBytes[8:10], idBytes[10:]) 49 | fmt.Printf("Entering function %s called by %s with args (%v) (%v); callID=%s\n", funcName, caller, i, b, callID) 50 | defer fmt.Printf("Exiting function %s called by %s; callID=%s\n", funcName, caller, callID) /* prinTracer */ 51 | 52 | if b { 53 | return i 54 | } 55 | return 0 56 | } 57 | 58 | func main() { 59 | 60 | /* prinTracer */ 61 | funcName := "main" 62 | caller := "unknown" 63 | if funcPC, _, _, ok := rt.Caller(0); ok { 64 | funcName = rt.FuncForPC(funcPC).Name() 65 | } 66 | if callerPC, _, _, ok := rt.Caller(1); ok { 67 | caller = rt.FuncForPC(callerPC).Name() 68 | } 69 | idBytes := make([]byte, 16) 70 | _, _ = rand.Read(idBytes) 71 | callID := fmt.Sprintf("%x-%x-%x-%x-%x", idBytes[0:4], idBytes[4:6], idBytes[6:8], idBytes[8:10], idBytes[10:]) 72 | fmt.Printf("Entering function %s called by %s; callID=%s\n", funcName, caller, callID) 73 | defer fmt.Printf("Exiting function %s called by %s; callID=%s\n", funcName, caller, callID) /* prinTracer */ 74 | 75 | i := test(2, false) 76 | } 77 | ` 78 | 79 | const editedResultCodeWithoutImports = `package a 80 | 81 | import ( 82 | "crypto/rand" 83 | "fmt" 84 | rt "runtime" 85 | ) 86 | 87 | func test(i int, b bool) int { 88 | 89 | /* prinTracer */ 90 | funcName := "test2" 91 | caller := "unknown2" 92 | if funcPC, _, _, ok := rt.Caller(0); ok { 93 | funcName = rt.FuncForPC(funcPC).Name() 94 | } 95 | if callerPC, _, _, ok := rt.Caller(1); ok { 96 | caller = rt.FuncForPC(callerPC).Name() 97 | } 98 | fmt.Println("test") 99 | idBytes := make([]byte, 16) 100 | _, _ = rand.Read(idBytes) 101 | callID := fmt.Sprintf("%x-%x-%x-%x-%x", idBytes[0:4], idBytes[4:6], idBytes[6:8], idBytes[8:10], idBytes[10:]) 102 | fmt.Printf("Entering function %s called by %s with args (%v) (%v); callID=%s\n", funcName, caller, i, b, callID) 103 | defer fmt.Printf("Exiting function %s called by %s; callID=%s\n", funcName, caller, callID) /* prinTracer */ 104 | 105 | if b { 106 | return i 107 | } 108 | return 0 109 | } 110 | 111 | func main() { 112 | 113 | funcName := "main" 114 | caller := "unknown" 115 | if funcPC, _, _, ok := rt.Caller(0); ok { 116 | funcName = rt.FuncForPC(funcPC).Name() 117 | } 118 | if callerPC, _, _, ok := rt.Caller(1); ok { 119 | caller = rt.FuncForPC(callerPC).Name() 120 | } 121 | idBytes := make([]byte, 16) 122 | _, _ = rand.Read(idBytes) 123 | callID := fmt.Sprintf("%x-%x-%x-%x-%x", idBytes[0:4], idBytes[4:6], idBytes[6:8], idBytes[8:10], idBytes[10:]) 124 | fmt.Printf("Entering function %s called by %s; callID=%s\n", funcName, caller, callID) 125 | defer fmt.Printf("Exiting function %s called by %s; callID=%s\n", funcName, caller, callID) 126 | 127 | i := test(2, false) 128 | } 129 | ` 130 | 131 | const codeWithFmtImport = `package a 132 | 133 | import ( 134 | "fmt" 135 | ) 136 | 137 | func test(i int, b bool) int { 138 | if b { 139 | return i 140 | } 141 | return 0 142 | } 143 | 144 | func main() { 145 | i := test(2, false) 146 | fmt.Println(i) 147 | } 148 | ` 149 | const resultCodeWithFmtImport = `package a 150 | 151 | import ( 152 | "crypto/rand" 153 | "fmt" 154 | rt "runtime" 155 | ) 156 | 157 | func test(i int, b bool) int { 158 | 159 | /* prinTracer */ 160 | funcName := "test" 161 | caller := "unknown" 162 | if funcPC, _, _, ok := rt.Caller(0); ok { 163 | funcName = rt.FuncForPC(funcPC).Name() 164 | } 165 | if callerPC, _, _, ok := rt.Caller(1); ok { 166 | caller = rt.FuncForPC(callerPC).Name() 167 | } 168 | idBytes := make([]byte, 16) 169 | _, _ = rand.Read(idBytes) 170 | callID := fmt.Sprintf("%x-%x-%x-%x-%x", idBytes[0:4], idBytes[4:6], idBytes[6:8], idBytes[8:10], idBytes[10:]) 171 | fmt.Printf("Entering function %s called by %s with args (%v) (%v); callID=%s\n", funcName, caller, i, b, callID) 172 | defer fmt.Printf("Exiting function %s called by %s; callID=%s\n", funcName, caller, callID) /* prinTracer */ 173 | 174 | if b { 175 | return i 176 | } 177 | return 0 178 | } 179 | 180 | func main() { 181 | 182 | /* prinTracer */ 183 | funcName := "main" 184 | caller := "unknown" 185 | if funcPC, _, _, ok := rt.Caller(0); ok { 186 | funcName = rt.FuncForPC(funcPC).Name() 187 | } 188 | if callerPC, _, _, ok := rt.Caller(1); ok { 189 | caller = rt.FuncForPC(callerPC).Name() 190 | } 191 | idBytes := make([]byte, 16) 192 | _, _ = rand.Read(idBytes) 193 | callID := fmt.Sprintf("%x-%x-%x-%x-%x", idBytes[0:4], idBytes[4:6], idBytes[6:8], idBytes[8:10], idBytes[10:]) 194 | fmt.Printf("Entering function %s called by %s; callID=%s\n", funcName, caller, callID) 195 | defer fmt.Printf("Exiting function %s called by %s; callID=%s\n", funcName, caller, callID) /* prinTracer */ 196 | 197 | i := test(2, false) 198 | fmt.Println(i) 199 | } 200 | ` 201 | 202 | const codeWithMultipleImports = `package a 203 | 204 | import ( 205 | "fmt" 206 | "strconv" 207 | ) 208 | 209 | func test(i int, b bool) int { 210 | if b { 211 | return i 212 | } 213 | return 0 214 | } 215 | 216 | func main() { 217 | i := test(2, false) 218 | fmt.Println(strconv.Itoa(i)) 219 | } 220 | ` 221 | 222 | const resultCodeWithMultipleImports = `package a 223 | 224 | import ( 225 | "crypto/rand" 226 | "fmt" 227 | rt "runtime" 228 | "strconv" 229 | ) 230 | 231 | func test(i int, b bool) int { 232 | 233 | /* prinTracer */ 234 | funcName := "test" 235 | caller := "unknown" 236 | if funcPC, _, _, ok := rt.Caller(0); ok { 237 | funcName = rt.FuncForPC(funcPC).Name() 238 | } 239 | if callerPC, _, _, ok := rt.Caller(1); ok { 240 | caller = rt.FuncForPC(callerPC).Name() 241 | } 242 | idBytes := make([]byte, 16) 243 | _, _ = rand.Read(idBytes) 244 | callID := fmt.Sprintf("%x-%x-%x-%x-%x", idBytes[0:4], idBytes[4:6], idBytes[6:8], idBytes[8:10], idBytes[10:]) 245 | fmt.Printf("Entering function %s called by %s with args (%v) (%v); callID=%s\n", funcName, caller, i, b, callID) 246 | defer fmt.Printf("Exiting function %s called by %s; callID=%s\n", funcName, caller, callID) /* prinTracer */ 247 | 248 | if b { 249 | return i 250 | } 251 | return 0 252 | } 253 | 254 | func main() { 255 | 256 | /* prinTracer */ 257 | funcName := "main" 258 | caller := "unknown" 259 | if funcPC, _, _, ok := rt.Caller(0); ok { 260 | funcName = rt.FuncForPC(funcPC).Name() 261 | } 262 | if callerPC, _, _, ok := rt.Caller(1); ok { 263 | caller = rt.FuncForPC(callerPC).Name() 264 | } 265 | idBytes := make([]byte, 16) 266 | _, _ = rand.Read(idBytes) 267 | callID := fmt.Sprintf("%x-%x-%x-%x-%x", idBytes[0:4], idBytes[4:6], idBytes[6:8], idBytes[8:10], idBytes[10:]) 268 | fmt.Printf("Entering function %s called by %s; callID=%s\n", funcName, caller, callID) 269 | defer fmt.Printf("Exiting function %s called by %s; callID=%s\n", funcName, caller, callID) /* prinTracer */ 270 | 271 | i := test(2, false) 272 | fmt.Println(strconv.Itoa(i)) 273 | } 274 | ` 275 | 276 | const codeWithImportsWithoutFmt = `package a 277 | 278 | import ( 279 | "strconv" 280 | ) 281 | 282 | func test(i int, b bool) int { 283 | if b { 284 | return i 285 | } 286 | return 0 287 | } 288 | 289 | func main() { 290 | i := test(2, false) 291 | s := strconv.Itoa(i) 292 | } 293 | ` 294 | 295 | const resultCodeWithImportsWithoutFmt = `package a 296 | 297 | import ( 298 | "crypto/rand" 299 | "fmt" 300 | rt "runtime" 301 | "strconv" 302 | ) 303 | 304 | func test(i int, b bool) int { 305 | 306 | /* prinTracer */ 307 | funcName := "test" 308 | caller := "unknown" 309 | if funcPC, _, _, ok := rt.Caller(0); ok { 310 | funcName = rt.FuncForPC(funcPC).Name() 311 | } 312 | if callerPC, _, _, ok := rt.Caller(1); ok { 313 | caller = rt.FuncForPC(callerPC).Name() 314 | } 315 | idBytes := make([]byte, 16) 316 | _, _ = rand.Read(idBytes) 317 | callID := fmt.Sprintf("%x-%x-%x-%x-%x", idBytes[0:4], idBytes[4:6], idBytes[6:8], idBytes[8:10], idBytes[10:]) 318 | fmt.Printf("Entering function %s called by %s with args (%v) (%v); callID=%s\n", funcName, caller, i, b, callID) 319 | defer fmt.Printf("Exiting function %s called by %s; callID=%s\n", funcName, caller, callID) /* prinTracer */ 320 | 321 | if b { 322 | return i 323 | } 324 | return 0 325 | } 326 | 327 | func main() { 328 | 329 | /* prinTracer */ 330 | funcName := "main" 331 | caller := "unknown" 332 | if funcPC, _, _, ok := rt.Caller(0); ok { 333 | funcName = rt.FuncForPC(funcPC).Name() 334 | } 335 | if callerPC, _, _, ok := rt.Caller(1); ok { 336 | caller = rt.FuncForPC(callerPC).Name() 337 | } 338 | idBytes := make([]byte, 16) 339 | _, _ = rand.Read(idBytes) 340 | callID := fmt.Sprintf("%x-%x-%x-%x-%x", idBytes[0:4], idBytes[4:6], idBytes[6:8], idBytes[8:10], idBytes[10:]) 341 | fmt.Printf("Entering function %s called by %s; callID=%s\n", funcName, caller, callID) 342 | defer fmt.Printf("Exiting function %s called by %s; callID=%s\n", funcName, caller, callID) /* prinTracer */ 343 | 344 | i := test(2, false) 345 | s := strconv.Itoa(i) 346 | } 347 | ` 348 | 349 | const codeWithWatermarks = `package a 350 | 351 | import ( 352 | "crypto/rand" 353 | "fmt" 354 | rt "runtime" 355 | ) 356 | 357 | func test(i int, b bool) int { 358 | /* prinTracer */ 359 | if b { 360 | return i 361 | } 362 | return 0 363 | } 364 | 365 | func main() { 366 | /* prinTracer */ 367 | i := test(2, false) 368 | } 369 | ` 370 | 371 | const codeWithoutFunction = `package a 372 | 373 | type test struct { 374 | a int 375 | } 376 | ` 377 | 378 | const resultCodeWithoutFunction = `package a 379 | 380 | import ( 381 | "crypto/rand" 382 | "fmt" 383 | rt "runtime" 384 | ) 385 | 386 | type test struct { 387 | a int 388 | } 389 | ` 390 | 391 | func TestInstrumentFile(t *testing.T) { 392 | tests := []struct { 393 | Name string 394 | InputCode string 395 | OutputCode string 396 | }{ 397 | {Name: "InstrumentFileWithoutImports", InputCode: codeWithoutImports, OutputCode: resultCodeWithoutImports}, 398 | {Name: "InstrumentFileWithFmtImportOnly", InputCode: codeWithFmtImport, OutputCode: resultCodeWithFmtImport}, 399 | {Name: "InstrumentFileWithMultipleImports", InputCode: codeWithMultipleImports, OutputCode: resultCodeWithMultipleImports}, 400 | {Name: "InstrumentFileWithoutFmtImport", InputCode: codeWithImportsWithoutFmt, OutputCode: resultCodeWithImportsWithoutFmt}, 401 | {Name: "InstrumentFileWithoutFunctions", InputCode: codeWithoutFunction, OutputCode: resultCodeWithoutFunction}, 402 | {Name: "InstrumentFileDoesNotAffectAlreadyInstrumentedFiles", InputCode: resultCodeWithFmtImport, OutputCode: resultCodeWithFmtImport}, 403 | {Name: "FunctionsWithWatermarksShouldNotBeInstrumented", InputCode: codeWithWatermarks, OutputCode: codeWithWatermarks}, 404 | } 405 | 406 | for _, test := range tests { 407 | t.Run(test.Name, func(t *testing.T) { 408 | fset := token.NewFileSet() 409 | file, err := parser.ParseFile(fset, "", test.InputCode, parser.ParseComments) 410 | if err != nil { 411 | t.Fatal(err) 412 | } 413 | var buff bytes.Buffer 414 | if err := NewCodeInstrumenter().InstrumentFile(fset, file, &buff); err != nil { 415 | t.Fatal(err) 416 | } 417 | 418 | if buff.String() != test.OutputCode { 419 | t.Errorf("Assertion failed! Expected %s got %s", test.OutputCode, buff.String()) 420 | } 421 | }) 422 | } 423 | } 424 | 425 | func TestInstrumentDirectory(t *testing.T) { 426 | if err := os.Mkdir("test", 0777); err != nil { 427 | t.Fatal(err) 428 | } 429 | defer func() { 430 | if err := os.RemoveAll("test"); err != nil { 431 | t.Fatal(err) 432 | } 433 | }() 434 | 435 | filePairs := []struct { 436 | InputCode string 437 | OutputCode string 438 | }{ 439 | {InputCode: codeWithoutImports, OutputCode: resultCodeWithoutImports}, 440 | {InputCode: codeWithFmtImport, OutputCode: resultCodeWithFmtImport}, 441 | {InputCode: codeWithMultipleImports, OutputCode: resultCodeWithMultipleImports}, 442 | {InputCode: codeWithImportsWithoutFmt, OutputCode: resultCodeWithImportsWithoutFmt}, 443 | {InputCode: codeWithoutFunction, OutputCode: resultCodeWithoutFunction}, 444 | {InputCode: resultCodeWithFmtImport, OutputCode: resultCodeWithFmtImport}, 445 | } 446 | 447 | i := 0 448 | for _, filePair := range filePairs { 449 | if err := ioutil.WriteFile(fmt.Sprintf("test/test%d.go", i), []byte(filePair.InputCode), 0777); err != nil { 450 | t.Fatal(err) 451 | } 452 | i++ 453 | } 454 | 455 | if err := NewCodeInstrumenter().InstrumentDirectory("test"); err != nil { 456 | t.Fatal(err) 457 | } 458 | 459 | i = 0 460 | for _, filePair := range filePairs { 461 | data, err := ioutil.ReadFile(fmt.Sprintf("test/test%d.go", i)) 462 | if err != nil { 463 | t.Fatal(err) 464 | } 465 | if string(data) != filePair.OutputCode { 466 | t.Error("Assertion failed!") 467 | } 468 | i++ 469 | } 470 | } 471 | -------------------------------------------------------------------------------- /tracing/interfaces.go: -------------------------------------------------------------------------------- 1 | package tracing 2 | 3 | import ( 4 | "go/ast" 5 | "go/token" 6 | "io" 7 | ) 8 | 9 | //go:generate counterfeiter . CodeInstrumenter 10 | type CodeInstrumenter interface { 11 | InstrumentFile(fset *token.FileSet, file *ast.File, out io.Writer) error 12 | InstrumentPackage(fset *token.FileSet, pkg *ast.Package) error 13 | InstrumentDirectory(path string) error 14 | } 15 | 16 | //go:generate counterfeiter . CodeDeinstrumenter 17 | type CodeDeinstrumenter interface { 18 | DeinstrumentFile(fset *token.FileSet, file *ast.File, out io.Writer) error 19 | DeinstrumentPackage(fset *token.FileSet, pkg *ast.Package) error 20 | DeinstrumentDirectory(path string) error 21 | } 22 | 23 | //go:generate counterfeiter . ImportsGroomer 24 | type ImportsGroomer interface { 25 | RemoveUnusedImportFromFile(fset *token.FileSet, file *ast.File, out io.Writer, importsToRemove map[string]string) error 26 | RemoveUnusedImportFromPackage(fset *token.FileSet, pkg *ast.Package, importsToRemove map[string]string) error 27 | RemoveUnusedImportFromDirectory(path string, importsToRemove map[string]string) error 28 | } 29 | -------------------------------------------------------------------------------- /tracing/tracingfakes/fake_code_deinstrumenter.go: -------------------------------------------------------------------------------- 1 | // Code generated by counterfeiter. DO NOT EDIT. 2 | package tracingfakes 3 | 4 | import ( 5 | "go/ast" 6 | "go/token" 7 | "io" 8 | "sync" 9 | 10 | "github.com/DimitarPetrov/printracer/tracing" 11 | ) 12 | 13 | type FakeCodeDeinstrumenter struct { 14 | DeinstrumentDirectoryStub func(string) error 15 | deinstrumentDirectoryMutex sync.RWMutex 16 | deinstrumentDirectoryArgsForCall []struct { 17 | arg1 string 18 | } 19 | deinstrumentDirectoryReturns struct { 20 | result1 error 21 | } 22 | deinstrumentDirectoryReturnsOnCall map[int]struct { 23 | result1 error 24 | } 25 | DeinstrumentFileStub func(*token.FileSet, *ast.File, io.Writer) error 26 | deinstrumentFileMutex sync.RWMutex 27 | deinstrumentFileArgsForCall []struct { 28 | arg1 *token.FileSet 29 | arg2 *ast.File 30 | arg3 io.Writer 31 | } 32 | deinstrumentFileReturns struct { 33 | result1 error 34 | } 35 | deinstrumentFileReturnsOnCall map[int]struct { 36 | result1 error 37 | } 38 | DeinstrumentPackageStub func(*token.FileSet, *ast.Package) error 39 | deinstrumentPackageMutex sync.RWMutex 40 | deinstrumentPackageArgsForCall []struct { 41 | arg1 *token.FileSet 42 | arg2 *ast.Package 43 | } 44 | deinstrumentPackageReturns struct { 45 | result1 error 46 | } 47 | deinstrumentPackageReturnsOnCall map[int]struct { 48 | result1 error 49 | } 50 | invocations map[string][][]interface{} 51 | invocationsMutex sync.RWMutex 52 | } 53 | 54 | func (fake *FakeCodeDeinstrumenter) DeinstrumentDirectory(arg1 string) error { 55 | fake.deinstrumentDirectoryMutex.Lock() 56 | ret, specificReturn := fake.deinstrumentDirectoryReturnsOnCall[len(fake.deinstrumentDirectoryArgsForCall)] 57 | fake.deinstrumentDirectoryArgsForCall = append(fake.deinstrumentDirectoryArgsForCall, struct { 58 | arg1 string 59 | }{arg1}) 60 | fake.recordInvocation("DeinstrumentDirectory", []interface{}{arg1}) 61 | fake.deinstrumentDirectoryMutex.Unlock() 62 | if fake.DeinstrumentDirectoryStub != nil { 63 | return fake.DeinstrumentDirectoryStub(arg1) 64 | } 65 | if specificReturn { 66 | return ret.result1 67 | } 68 | fakeReturns := fake.deinstrumentDirectoryReturns 69 | return fakeReturns.result1 70 | } 71 | 72 | func (fake *FakeCodeDeinstrumenter) DeinstrumentDirectoryCallCount() int { 73 | fake.deinstrumentDirectoryMutex.RLock() 74 | defer fake.deinstrumentDirectoryMutex.RUnlock() 75 | return len(fake.deinstrumentDirectoryArgsForCall) 76 | } 77 | 78 | func (fake *FakeCodeDeinstrumenter) DeinstrumentDirectoryCalls(stub func(string) error) { 79 | fake.deinstrumentDirectoryMutex.Lock() 80 | defer fake.deinstrumentDirectoryMutex.Unlock() 81 | fake.DeinstrumentDirectoryStub = stub 82 | } 83 | 84 | func (fake *FakeCodeDeinstrumenter) DeinstrumentDirectoryArgsForCall(i int) string { 85 | fake.deinstrumentDirectoryMutex.RLock() 86 | defer fake.deinstrumentDirectoryMutex.RUnlock() 87 | argsForCall := fake.deinstrumentDirectoryArgsForCall[i] 88 | return argsForCall.arg1 89 | } 90 | 91 | func (fake *FakeCodeDeinstrumenter) DeinstrumentDirectoryReturns(result1 error) { 92 | fake.deinstrumentDirectoryMutex.Lock() 93 | defer fake.deinstrumentDirectoryMutex.Unlock() 94 | fake.DeinstrumentDirectoryStub = nil 95 | fake.deinstrumentDirectoryReturns = struct { 96 | result1 error 97 | }{result1} 98 | } 99 | 100 | func (fake *FakeCodeDeinstrumenter) DeinstrumentDirectoryReturnsOnCall(i int, result1 error) { 101 | fake.deinstrumentDirectoryMutex.Lock() 102 | defer fake.deinstrumentDirectoryMutex.Unlock() 103 | fake.DeinstrumentDirectoryStub = nil 104 | if fake.deinstrumentDirectoryReturnsOnCall == nil { 105 | fake.deinstrumentDirectoryReturnsOnCall = make(map[int]struct { 106 | result1 error 107 | }) 108 | } 109 | fake.deinstrumentDirectoryReturnsOnCall[i] = struct { 110 | result1 error 111 | }{result1} 112 | } 113 | 114 | func (fake *FakeCodeDeinstrumenter) DeinstrumentFile(arg1 *token.FileSet, arg2 *ast.File, arg3 io.Writer) error { 115 | fake.deinstrumentFileMutex.Lock() 116 | ret, specificReturn := fake.deinstrumentFileReturnsOnCall[len(fake.deinstrumentFileArgsForCall)] 117 | fake.deinstrumentFileArgsForCall = append(fake.deinstrumentFileArgsForCall, struct { 118 | arg1 *token.FileSet 119 | arg2 *ast.File 120 | arg3 io.Writer 121 | }{arg1, arg2, arg3}) 122 | fake.recordInvocation("DeinstrumentFile", []interface{}{arg1, arg2, arg3}) 123 | fake.deinstrumentFileMutex.Unlock() 124 | if fake.DeinstrumentFileStub != nil { 125 | return fake.DeinstrumentFileStub(arg1, arg2, arg3) 126 | } 127 | if specificReturn { 128 | return ret.result1 129 | } 130 | fakeReturns := fake.deinstrumentFileReturns 131 | return fakeReturns.result1 132 | } 133 | 134 | func (fake *FakeCodeDeinstrumenter) DeinstrumentFileCallCount() int { 135 | fake.deinstrumentFileMutex.RLock() 136 | defer fake.deinstrumentFileMutex.RUnlock() 137 | return len(fake.deinstrumentFileArgsForCall) 138 | } 139 | 140 | func (fake *FakeCodeDeinstrumenter) DeinstrumentFileCalls(stub func(*token.FileSet, *ast.File, io.Writer) error) { 141 | fake.deinstrumentFileMutex.Lock() 142 | defer fake.deinstrumentFileMutex.Unlock() 143 | fake.DeinstrumentFileStub = stub 144 | } 145 | 146 | func (fake *FakeCodeDeinstrumenter) DeinstrumentFileArgsForCall(i int) (*token.FileSet, *ast.File, io.Writer) { 147 | fake.deinstrumentFileMutex.RLock() 148 | defer fake.deinstrumentFileMutex.RUnlock() 149 | argsForCall := fake.deinstrumentFileArgsForCall[i] 150 | return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 151 | } 152 | 153 | func (fake *FakeCodeDeinstrumenter) DeinstrumentFileReturns(result1 error) { 154 | fake.deinstrumentFileMutex.Lock() 155 | defer fake.deinstrumentFileMutex.Unlock() 156 | fake.DeinstrumentFileStub = nil 157 | fake.deinstrumentFileReturns = struct { 158 | result1 error 159 | }{result1} 160 | } 161 | 162 | func (fake *FakeCodeDeinstrumenter) DeinstrumentFileReturnsOnCall(i int, result1 error) { 163 | fake.deinstrumentFileMutex.Lock() 164 | defer fake.deinstrumentFileMutex.Unlock() 165 | fake.DeinstrumentFileStub = nil 166 | if fake.deinstrumentFileReturnsOnCall == nil { 167 | fake.deinstrumentFileReturnsOnCall = make(map[int]struct { 168 | result1 error 169 | }) 170 | } 171 | fake.deinstrumentFileReturnsOnCall[i] = struct { 172 | result1 error 173 | }{result1} 174 | } 175 | 176 | func (fake *FakeCodeDeinstrumenter) DeinstrumentPackage(arg1 *token.FileSet, arg2 *ast.Package) error { 177 | fake.deinstrumentPackageMutex.Lock() 178 | ret, specificReturn := fake.deinstrumentPackageReturnsOnCall[len(fake.deinstrumentPackageArgsForCall)] 179 | fake.deinstrumentPackageArgsForCall = append(fake.deinstrumentPackageArgsForCall, struct { 180 | arg1 *token.FileSet 181 | arg2 *ast.Package 182 | }{arg1, arg2}) 183 | fake.recordInvocation("DeinstrumentPackage", []interface{}{arg1, arg2}) 184 | fake.deinstrumentPackageMutex.Unlock() 185 | if fake.DeinstrumentPackageStub != nil { 186 | return fake.DeinstrumentPackageStub(arg1, arg2) 187 | } 188 | if specificReturn { 189 | return ret.result1 190 | } 191 | fakeReturns := fake.deinstrumentPackageReturns 192 | return fakeReturns.result1 193 | } 194 | 195 | func (fake *FakeCodeDeinstrumenter) DeinstrumentPackageCallCount() int { 196 | fake.deinstrumentPackageMutex.RLock() 197 | defer fake.deinstrumentPackageMutex.RUnlock() 198 | return len(fake.deinstrumentPackageArgsForCall) 199 | } 200 | 201 | func (fake *FakeCodeDeinstrumenter) DeinstrumentPackageCalls(stub func(*token.FileSet, *ast.Package) error) { 202 | fake.deinstrumentPackageMutex.Lock() 203 | defer fake.deinstrumentPackageMutex.Unlock() 204 | fake.DeinstrumentPackageStub = stub 205 | } 206 | 207 | func (fake *FakeCodeDeinstrumenter) DeinstrumentPackageArgsForCall(i int) (*token.FileSet, *ast.Package) { 208 | fake.deinstrumentPackageMutex.RLock() 209 | defer fake.deinstrumentPackageMutex.RUnlock() 210 | argsForCall := fake.deinstrumentPackageArgsForCall[i] 211 | return argsForCall.arg1, argsForCall.arg2 212 | } 213 | 214 | func (fake *FakeCodeDeinstrumenter) DeinstrumentPackageReturns(result1 error) { 215 | fake.deinstrumentPackageMutex.Lock() 216 | defer fake.deinstrumentPackageMutex.Unlock() 217 | fake.DeinstrumentPackageStub = nil 218 | fake.deinstrumentPackageReturns = struct { 219 | result1 error 220 | }{result1} 221 | } 222 | 223 | func (fake *FakeCodeDeinstrumenter) DeinstrumentPackageReturnsOnCall(i int, result1 error) { 224 | fake.deinstrumentPackageMutex.Lock() 225 | defer fake.deinstrumentPackageMutex.Unlock() 226 | fake.DeinstrumentPackageStub = nil 227 | if fake.deinstrumentPackageReturnsOnCall == nil { 228 | fake.deinstrumentPackageReturnsOnCall = make(map[int]struct { 229 | result1 error 230 | }) 231 | } 232 | fake.deinstrumentPackageReturnsOnCall[i] = struct { 233 | result1 error 234 | }{result1} 235 | } 236 | 237 | func (fake *FakeCodeDeinstrumenter) Invocations() map[string][][]interface{} { 238 | fake.invocationsMutex.RLock() 239 | defer fake.invocationsMutex.RUnlock() 240 | fake.deinstrumentDirectoryMutex.RLock() 241 | defer fake.deinstrumentDirectoryMutex.RUnlock() 242 | fake.deinstrumentFileMutex.RLock() 243 | defer fake.deinstrumentFileMutex.RUnlock() 244 | fake.deinstrumentPackageMutex.RLock() 245 | defer fake.deinstrumentPackageMutex.RUnlock() 246 | copiedInvocations := map[string][][]interface{}{} 247 | for key, value := range fake.invocations { 248 | copiedInvocations[key] = value 249 | } 250 | return copiedInvocations 251 | } 252 | 253 | func (fake *FakeCodeDeinstrumenter) recordInvocation(key string, args []interface{}) { 254 | fake.invocationsMutex.Lock() 255 | defer fake.invocationsMutex.Unlock() 256 | if fake.invocations == nil { 257 | fake.invocations = map[string][][]interface{}{} 258 | } 259 | if fake.invocations[key] == nil { 260 | fake.invocations[key] = [][]interface{}{} 261 | } 262 | fake.invocations[key] = append(fake.invocations[key], args) 263 | } 264 | 265 | var _ tracing.CodeDeinstrumenter = new(FakeCodeDeinstrumenter) 266 | -------------------------------------------------------------------------------- /tracing/tracingfakes/fake_code_instrumenter.go: -------------------------------------------------------------------------------- 1 | // Code generated by counterfeiter. DO NOT EDIT. 2 | package tracingfakes 3 | 4 | import ( 5 | "go/ast" 6 | "go/token" 7 | "io" 8 | "sync" 9 | 10 | "github.com/DimitarPetrov/printracer/tracing" 11 | ) 12 | 13 | type FakeCodeInstrumenter struct { 14 | InstrumentDirectoryStub func(string) error 15 | instrumentDirectoryMutex sync.RWMutex 16 | instrumentDirectoryArgsForCall []struct { 17 | arg1 string 18 | } 19 | instrumentDirectoryReturns struct { 20 | result1 error 21 | } 22 | instrumentDirectoryReturnsOnCall map[int]struct { 23 | result1 error 24 | } 25 | InstrumentFileStub func(*token.FileSet, *ast.File, io.Writer) error 26 | instrumentFileMutex sync.RWMutex 27 | instrumentFileArgsForCall []struct { 28 | arg1 *token.FileSet 29 | arg2 *ast.File 30 | arg3 io.Writer 31 | } 32 | instrumentFileReturns struct { 33 | result1 error 34 | } 35 | instrumentFileReturnsOnCall map[int]struct { 36 | result1 error 37 | } 38 | InstrumentPackageStub func(*token.FileSet, *ast.Package) error 39 | instrumentPackageMutex sync.RWMutex 40 | instrumentPackageArgsForCall []struct { 41 | arg1 *token.FileSet 42 | arg2 *ast.Package 43 | } 44 | instrumentPackageReturns struct { 45 | result1 error 46 | } 47 | instrumentPackageReturnsOnCall map[int]struct { 48 | result1 error 49 | } 50 | invocations map[string][][]interface{} 51 | invocationsMutex sync.RWMutex 52 | } 53 | 54 | func (fake *FakeCodeInstrumenter) InstrumentDirectory(arg1 string) error { 55 | fake.instrumentDirectoryMutex.Lock() 56 | ret, specificReturn := fake.instrumentDirectoryReturnsOnCall[len(fake.instrumentDirectoryArgsForCall)] 57 | fake.instrumentDirectoryArgsForCall = append(fake.instrumentDirectoryArgsForCall, struct { 58 | arg1 string 59 | }{arg1}) 60 | fake.recordInvocation("InstrumentDirectory", []interface{}{arg1}) 61 | fake.instrumentDirectoryMutex.Unlock() 62 | if fake.InstrumentDirectoryStub != nil { 63 | return fake.InstrumentDirectoryStub(arg1) 64 | } 65 | if specificReturn { 66 | return ret.result1 67 | } 68 | fakeReturns := fake.instrumentDirectoryReturns 69 | return fakeReturns.result1 70 | } 71 | 72 | func (fake *FakeCodeInstrumenter) InstrumentDirectoryCallCount() int { 73 | fake.instrumentDirectoryMutex.RLock() 74 | defer fake.instrumentDirectoryMutex.RUnlock() 75 | return len(fake.instrumentDirectoryArgsForCall) 76 | } 77 | 78 | func (fake *FakeCodeInstrumenter) InstrumentDirectoryCalls(stub func(string) error) { 79 | fake.instrumentDirectoryMutex.Lock() 80 | defer fake.instrumentDirectoryMutex.Unlock() 81 | fake.InstrumentDirectoryStub = stub 82 | } 83 | 84 | func (fake *FakeCodeInstrumenter) InstrumentDirectoryArgsForCall(i int) string { 85 | fake.instrumentDirectoryMutex.RLock() 86 | defer fake.instrumentDirectoryMutex.RUnlock() 87 | argsForCall := fake.instrumentDirectoryArgsForCall[i] 88 | return argsForCall.arg1 89 | } 90 | 91 | func (fake *FakeCodeInstrumenter) InstrumentDirectoryReturns(result1 error) { 92 | fake.instrumentDirectoryMutex.Lock() 93 | defer fake.instrumentDirectoryMutex.Unlock() 94 | fake.InstrumentDirectoryStub = nil 95 | fake.instrumentDirectoryReturns = struct { 96 | result1 error 97 | }{result1} 98 | } 99 | 100 | func (fake *FakeCodeInstrumenter) InstrumentDirectoryReturnsOnCall(i int, result1 error) { 101 | fake.instrumentDirectoryMutex.Lock() 102 | defer fake.instrumentDirectoryMutex.Unlock() 103 | fake.InstrumentDirectoryStub = nil 104 | if fake.instrumentDirectoryReturnsOnCall == nil { 105 | fake.instrumentDirectoryReturnsOnCall = make(map[int]struct { 106 | result1 error 107 | }) 108 | } 109 | fake.instrumentDirectoryReturnsOnCall[i] = struct { 110 | result1 error 111 | }{result1} 112 | } 113 | 114 | func (fake *FakeCodeInstrumenter) InstrumentFile(arg1 *token.FileSet, arg2 *ast.File, arg3 io.Writer) error { 115 | fake.instrumentFileMutex.Lock() 116 | ret, specificReturn := fake.instrumentFileReturnsOnCall[len(fake.instrumentFileArgsForCall)] 117 | fake.instrumentFileArgsForCall = append(fake.instrumentFileArgsForCall, struct { 118 | arg1 *token.FileSet 119 | arg2 *ast.File 120 | arg3 io.Writer 121 | }{arg1, arg2, arg3}) 122 | fake.recordInvocation("InstrumentFile", []interface{}{arg1, arg2, arg3}) 123 | fake.instrumentFileMutex.Unlock() 124 | if fake.InstrumentFileStub != nil { 125 | return fake.InstrumentFileStub(arg1, arg2, arg3) 126 | } 127 | if specificReturn { 128 | return ret.result1 129 | } 130 | fakeReturns := fake.instrumentFileReturns 131 | return fakeReturns.result1 132 | } 133 | 134 | func (fake *FakeCodeInstrumenter) InstrumentFileCallCount() int { 135 | fake.instrumentFileMutex.RLock() 136 | defer fake.instrumentFileMutex.RUnlock() 137 | return len(fake.instrumentFileArgsForCall) 138 | } 139 | 140 | func (fake *FakeCodeInstrumenter) InstrumentFileCalls(stub func(*token.FileSet, *ast.File, io.Writer) error) { 141 | fake.instrumentFileMutex.Lock() 142 | defer fake.instrumentFileMutex.Unlock() 143 | fake.InstrumentFileStub = stub 144 | } 145 | 146 | func (fake *FakeCodeInstrumenter) InstrumentFileArgsForCall(i int) (*token.FileSet, *ast.File, io.Writer) { 147 | fake.instrumentFileMutex.RLock() 148 | defer fake.instrumentFileMutex.RUnlock() 149 | argsForCall := fake.instrumentFileArgsForCall[i] 150 | return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 151 | } 152 | 153 | func (fake *FakeCodeInstrumenter) InstrumentFileReturns(result1 error) { 154 | fake.instrumentFileMutex.Lock() 155 | defer fake.instrumentFileMutex.Unlock() 156 | fake.InstrumentFileStub = nil 157 | fake.instrumentFileReturns = struct { 158 | result1 error 159 | }{result1} 160 | } 161 | 162 | func (fake *FakeCodeInstrumenter) InstrumentFileReturnsOnCall(i int, result1 error) { 163 | fake.instrumentFileMutex.Lock() 164 | defer fake.instrumentFileMutex.Unlock() 165 | fake.InstrumentFileStub = nil 166 | if fake.instrumentFileReturnsOnCall == nil { 167 | fake.instrumentFileReturnsOnCall = make(map[int]struct { 168 | result1 error 169 | }) 170 | } 171 | fake.instrumentFileReturnsOnCall[i] = struct { 172 | result1 error 173 | }{result1} 174 | } 175 | 176 | func (fake *FakeCodeInstrumenter) InstrumentPackage(arg1 *token.FileSet, arg2 *ast.Package) error { 177 | fake.instrumentPackageMutex.Lock() 178 | ret, specificReturn := fake.instrumentPackageReturnsOnCall[len(fake.instrumentPackageArgsForCall)] 179 | fake.instrumentPackageArgsForCall = append(fake.instrumentPackageArgsForCall, struct { 180 | arg1 *token.FileSet 181 | arg2 *ast.Package 182 | }{arg1, arg2}) 183 | fake.recordInvocation("InstrumentPackage", []interface{}{arg1, arg2}) 184 | fake.instrumentPackageMutex.Unlock() 185 | if fake.InstrumentPackageStub != nil { 186 | return fake.InstrumentPackageStub(arg1, arg2) 187 | } 188 | if specificReturn { 189 | return ret.result1 190 | } 191 | fakeReturns := fake.instrumentPackageReturns 192 | return fakeReturns.result1 193 | } 194 | 195 | func (fake *FakeCodeInstrumenter) InstrumentPackageCallCount() int { 196 | fake.instrumentPackageMutex.RLock() 197 | defer fake.instrumentPackageMutex.RUnlock() 198 | return len(fake.instrumentPackageArgsForCall) 199 | } 200 | 201 | func (fake *FakeCodeInstrumenter) InstrumentPackageCalls(stub func(*token.FileSet, *ast.Package) error) { 202 | fake.instrumentPackageMutex.Lock() 203 | defer fake.instrumentPackageMutex.Unlock() 204 | fake.InstrumentPackageStub = stub 205 | } 206 | 207 | func (fake *FakeCodeInstrumenter) InstrumentPackageArgsForCall(i int) (*token.FileSet, *ast.Package) { 208 | fake.instrumentPackageMutex.RLock() 209 | defer fake.instrumentPackageMutex.RUnlock() 210 | argsForCall := fake.instrumentPackageArgsForCall[i] 211 | return argsForCall.arg1, argsForCall.arg2 212 | } 213 | 214 | func (fake *FakeCodeInstrumenter) InstrumentPackageReturns(result1 error) { 215 | fake.instrumentPackageMutex.Lock() 216 | defer fake.instrumentPackageMutex.Unlock() 217 | fake.InstrumentPackageStub = nil 218 | fake.instrumentPackageReturns = struct { 219 | result1 error 220 | }{result1} 221 | } 222 | 223 | func (fake *FakeCodeInstrumenter) InstrumentPackageReturnsOnCall(i int, result1 error) { 224 | fake.instrumentPackageMutex.Lock() 225 | defer fake.instrumentPackageMutex.Unlock() 226 | fake.InstrumentPackageStub = nil 227 | if fake.instrumentPackageReturnsOnCall == nil { 228 | fake.instrumentPackageReturnsOnCall = make(map[int]struct { 229 | result1 error 230 | }) 231 | } 232 | fake.instrumentPackageReturnsOnCall[i] = struct { 233 | result1 error 234 | }{result1} 235 | } 236 | 237 | func (fake *FakeCodeInstrumenter) Invocations() map[string][][]interface{} { 238 | fake.invocationsMutex.RLock() 239 | defer fake.invocationsMutex.RUnlock() 240 | fake.instrumentDirectoryMutex.RLock() 241 | defer fake.instrumentDirectoryMutex.RUnlock() 242 | fake.instrumentFileMutex.RLock() 243 | defer fake.instrumentFileMutex.RUnlock() 244 | fake.instrumentPackageMutex.RLock() 245 | defer fake.instrumentPackageMutex.RUnlock() 246 | copiedInvocations := map[string][][]interface{}{} 247 | for key, value := range fake.invocations { 248 | copiedInvocations[key] = value 249 | } 250 | return copiedInvocations 251 | } 252 | 253 | func (fake *FakeCodeInstrumenter) recordInvocation(key string, args []interface{}) { 254 | fake.invocationsMutex.Lock() 255 | defer fake.invocationsMutex.Unlock() 256 | if fake.invocations == nil { 257 | fake.invocations = map[string][][]interface{}{} 258 | } 259 | if fake.invocations[key] == nil { 260 | fake.invocations[key] = [][]interface{}{} 261 | } 262 | fake.invocations[key] = append(fake.invocations[key], args) 263 | } 264 | 265 | var _ tracing.CodeInstrumenter = new(FakeCodeInstrumenter) 266 | -------------------------------------------------------------------------------- /tracing/tracingfakes/fake_imports_groomer.go: -------------------------------------------------------------------------------- 1 | // Code generated by counterfeiter. DO NOT EDIT. 2 | package tracingfakes 3 | 4 | import ( 5 | "go/ast" 6 | "go/token" 7 | "io" 8 | "sync" 9 | 10 | "github.com/DimitarPetrov/printracer/tracing" 11 | ) 12 | 13 | type FakeImportsGroomer struct { 14 | RemoveUnusedImportFromDirectoryStub func(string, map[string]string) error 15 | removeUnusedImportFromDirectoryMutex sync.RWMutex 16 | removeUnusedImportFromDirectoryArgsForCall []struct { 17 | arg1 string 18 | arg2 map[string]string 19 | } 20 | removeUnusedImportFromDirectoryReturns struct { 21 | result1 error 22 | } 23 | removeUnusedImportFromDirectoryReturnsOnCall map[int]struct { 24 | result1 error 25 | } 26 | RemoveUnusedImportFromFileStub func(*token.FileSet, *ast.File, io.Writer, map[string]string) error 27 | removeUnusedImportFromFileMutex sync.RWMutex 28 | removeUnusedImportFromFileArgsForCall []struct { 29 | arg1 *token.FileSet 30 | arg2 *ast.File 31 | arg3 io.Writer 32 | arg4 map[string]string 33 | } 34 | removeUnusedImportFromFileReturns struct { 35 | result1 error 36 | } 37 | removeUnusedImportFromFileReturnsOnCall map[int]struct { 38 | result1 error 39 | } 40 | RemoveUnusedImportFromPackageStub func(*token.FileSet, *ast.Package, map[string]string) error 41 | removeUnusedImportFromPackageMutex sync.RWMutex 42 | removeUnusedImportFromPackageArgsForCall []struct { 43 | arg1 *token.FileSet 44 | arg2 *ast.Package 45 | arg3 map[string]string 46 | } 47 | removeUnusedImportFromPackageReturns struct { 48 | result1 error 49 | } 50 | removeUnusedImportFromPackageReturnsOnCall map[int]struct { 51 | result1 error 52 | } 53 | invocations map[string][][]interface{} 54 | invocationsMutex sync.RWMutex 55 | } 56 | 57 | func (fake *FakeImportsGroomer) RemoveUnusedImportFromDirectory(arg1 string, arg2 map[string]string) error { 58 | fake.removeUnusedImportFromDirectoryMutex.Lock() 59 | ret, specificReturn := fake.removeUnusedImportFromDirectoryReturnsOnCall[len(fake.removeUnusedImportFromDirectoryArgsForCall)] 60 | fake.removeUnusedImportFromDirectoryArgsForCall = append(fake.removeUnusedImportFromDirectoryArgsForCall, struct { 61 | arg1 string 62 | arg2 map[string]string 63 | }{arg1, arg2}) 64 | fake.recordInvocation("RemoveUnusedImportFromDirectory", []interface{}{arg1, arg2}) 65 | fake.removeUnusedImportFromDirectoryMutex.Unlock() 66 | if fake.RemoveUnusedImportFromDirectoryStub != nil { 67 | return fake.RemoveUnusedImportFromDirectoryStub(arg1, arg2) 68 | } 69 | if specificReturn { 70 | return ret.result1 71 | } 72 | fakeReturns := fake.removeUnusedImportFromDirectoryReturns 73 | return fakeReturns.result1 74 | } 75 | 76 | func (fake *FakeImportsGroomer) RemoveUnusedImportFromDirectoryCallCount() int { 77 | fake.removeUnusedImportFromDirectoryMutex.RLock() 78 | defer fake.removeUnusedImportFromDirectoryMutex.RUnlock() 79 | return len(fake.removeUnusedImportFromDirectoryArgsForCall) 80 | } 81 | 82 | func (fake *FakeImportsGroomer) RemoveUnusedImportFromDirectoryCalls(stub func(string, map[string]string) error) { 83 | fake.removeUnusedImportFromDirectoryMutex.Lock() 84 | defer fake.removeUnusedImportFromDirectoryMutex.Unlock() 85 | fake.RemoveUnusedImportFromDirectoryStub = stub 86 | } 87 | 88 | func (fake *FakeImportsGroomer) RemoveUnusedImportFromDirectoryArgsForCall(i int) (string, map[string]string) { 89 | fake.removeUnusedImportFromDirectoryMutex.RLock() 90 | defer fake.removeUnusedImportFromDirectoryMutex.RUnlock() 91 | argsForCall := fake.removeUnusedImportFromDirectoryArgsForCall[i] 92 | return argsForCall.arg1, argsForCall.arg2 93 | } 94 | 95 | func (fake *FakeImportsGroomer) RemoveUnusedImportFromDirectoryReturns(result1 error) { 96 | fake.removeUnusedImportFromDirectoryMutex.Lock() 97 | defer fake.removeUnusedImportFromDirectoryMutex.Unlock() 98 | fake.RemoveUnusedImportFromDirectoryStub = nil 99 | fake.removeUnusedImportFromDirectoryReturns = struct { 100 | result1 error 101 | }{result1} 102 | } 103 | 104 | func (fake *FakeImportsGroomer) RemoveUnusedImportFromDirectoryReturnsOnCall(i int, result1 error) { 105 | fake.removeUnusedImportFromDirectoryMutex.Lock() 106 | defer fake.removeUnusedImportFromDirectoryMutex.Unlock() 107 | fake.RemoveUnusedImportFromDirectoryStub = nil 108 | if fake.removeUnusedImportFromDirectoryReturnsOnCall == nil { 109 | fake.removeUnusedImportFromDirectoryReturnsOnCall = make(map[int]struct { 110 | result1 error 111 | }) 112 | } 113 | fake.removeUnusedImportFromDirectoryReturnsOnCall[i] = struct { 114 | result1 error 115 | }{result1} 116 | } 117 | 118 | func (fake *FakeImportsGroomer) RemoveUnusedImportFromFile(arg1 *token.FileSet, arg2 *ast.File, arg3 io.Writer, arg4 map[string]string) error { 119 | fake.removeUnusedImportFromFileMutex.Lock() 120 | ret, specificReturn := fake.removeUnusedImportFromFileReturnsOnCall[len(fake.removeUnusedImportFromFileArgsForCall)] 121 | fake.removeUnusedImportFromFileArgsForCall = append(fake.removeUnusedImportFromFileArgsForCall, struct { 122 | arg1 *token.FileSet 123 | arg2 *ast.File 124 | arg3 io.Writer 125 | arg4 map[string]string 126 | }{arg1, arg2, arg3, arg4}) 127 | fake.recordInvocation("RemoveUnusedImportFromFile", []interface{}{arg1, arg2, arg3, arg4}) 128 | fake.removeUnusedImportFromFileMutex.Unlock() 129 | if fake.RemoveUnusedImportFromFileStub != nil { 130 | return fake.RemoveUnusedImportFromFileStub(arg1, arg2, arg3, arg4) 131 | } 132 | if specificReturn { 133 | return ret.result1 134 | } 135 | fakeReturns := fake.removeUnusedImportFromFileReturns 136 | return fakeReturns.result1 137 | } 138 | 139 | func (fake *FakeImportsGroomer) RemoveUnusedImportFromFileCallCount() int { 140 | fake.removeUnusedImportFromFileMutex.RLock() 141 | defer fake.removeUnusedImportFromFileMutex.RUnlock() 142 | return len(fake.removeUnusedImportFromFileArgsForCall) 143 | } 144 | 145 | func (fake *FakeImportsGroomer) RemoveUnusedImportFromFileCalls(stub func(*token.FileSet, *ast.File, io.Writer, map[string]string) error) { 146 | fake.removeUnusedImportFromFileMutex.Lock() 147 | defer fake.removeUnusedImportFromFileMutex.Unlock() 148 | fake.RemoveUnusedImportFromFileStub = stub 149 | } 150 | 151 | func (fake *FakeImportsGroomer) RemoveUnusedImportFromFileArgsForCall(i int) (*token.FileSet, *ast.File, io.Writer, map[string]string) { 152 | fake.removeUnusedImportFromFileMutex.RLock() 153 | defer fake.removeUnusedImportFromFileMutex.RUnlock() 154 | argsForCall := fake.removeUnusedImportFromFileArgsForCall[i] 155 | return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4 156 | } 157 | 158 | func (fake *FakeImportsGroomer) RemoveUnusedImportFromFileReturns(result1 error) { 159 | fake.removeUnusedImportFromFileMutex.Lock() 160 | defer fake.removeUnusedImportFromFileMutex.Unlock() 161 | fake.RemoveUnusedImportFromFileStub = nil 162 | fake.removeUnusedImportFromFileReturns = struct { 163 | result1 error 164 | }{result1} 165 | } 166 | 167 | func (fake *FakeImportsGroomer) RemoveUnusedImportFromFileReturnsOnCall(i int, result1 error) { 168 | fake.removeUnusedImportFromFileMutex.Lock() 169 | defer fake.removeUnusedImportFromFileMutex.Unlock() 170 | fake.RemoveUnusedImportFromFileStub = nil 171 | if fake.removeUnusedImportFromFileReturnsOnCall == nil { 172 | fake.removeUnusedImportFromFileReturnsOnCall = make(map[int]struct { 173 | result1 error 174 | }) 175 | } 176 | fake.removeUnusedImportFromFileReturnsOnCall[i] = struct { 177 | result1 error 178 | }{result1} 179 | } 180 | 181 | func (fake *FakeImportsGroomer) RemoveUnusedImportFromPackage(arg1 *token.FileSet, arg2 *ast.Package, arg3 map[string]string) error { 182 | fake.removeUnusedImportFromPackageMutex.Lock() 183 | ret, specificReturn := fake.removeUnusedImportFromPackageReturnsOnCall[len(fake.removeUnusedImportFromPackageArgsForCall)] 184 | fake.removeUnusedImportFromPackageArgsForCall = append(fake.removeUnusedImportFromPackageArgsForCall, struct { 185 | arg1 *token.FileSet 186 | arg2 *ast.Package 187 | arg3 map[string]string 188 | }{arg1, arg2, arg3}) 189 | fake.recordInvocation("RemoveUnusedImportFromPackage", []interface{}{arg1, arg2, arg3}) 190 | fake.removeUnusedImportFromPackageMutex.Unlock() 191 | if fake.RemoveUnusedImportFromPackageStub != nil { 192 | return fake.RemoveUnusedImportFromPackageStub(arg1, arg2, arg3) 193 | } 194 | if specificReturn { 195 | return ret.result1 196 | } 197 | fakeReturns := fake.removeUnusedImportFromPackageReturns 198 | return fakeReturns.result1 199 | } 200 | 201 | func (fake *FakeImportsGroomer) RemoveUnusedImportFromPackageCallCount() int { 202 | fake.removeUnusedImportFromPackageMutex.RLock() 203 | defer fake.removeUnusedImportFromPackageMutex.RUnlock() 204 | return len(fake.removeUnusedImportFromPackageArgsForCall) 205 | } 206 | 207 | func (fake *FakeImportsGroomer) RemoveUnusedImportFromPackageCalls(stub func(*token.FileSet, *ast.Package, map[string]string) error) { 208 | fake.removeUnusedImportFromPackageMutex.Lock() 209 | defer fake.removeUnusedImportFromPackageMutex.Unlock() 210 | fake.RemoveUnusedImportFromPackageStub = stub 211 | } 212 | 213 | func (fake *FakeImportsGroomer) RemoveUnusedImportFromPackageArgsForCall(i int) (*token.FileSet, *ast.Package, map[string]string) { 214 | fake.removeUnusedImportFromPackageMutex.RLock() 215 | defer fake.removeUnusedImportFromPackageMutex.RUnlock() 216 | argsForCall := fake.removeUnusedImportFromPackageArgsForCall[i] 217 | return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 218 | } 219 | 220 | func (fake *FakeImportsGroomer) RemoveUnusedImportFromPackageReturns(result1 error) { 221 | fake.removeUnusedImportFromPackageMutex.Lock() 222 | defer fake.removeUnusedImportFromPackageMutex.Unlock() 223 | fake.RemoveUnusedImportFromPackageStub = nil 224 | fake.removeUnusedImportFromPackageReturns = struct { 225 | result1 error 226 | }{result1} 227 | } 228 | 229 | func (fake *FakeImportsGroomer) RemoveUnusedImportFromPackageReturnsOnCall(i int, result1 error) { 230 | fake.removeUnusedImportFromPackageMutex.Lock() 231 | defer fake.removeUnusedImportFromPackageMutex.Unlock() 232 | fake.RemoveUnusedImportFromPackageStub = nil 233 | if fake.removeUnusedImportFromPackageReturnsOnCall == nil { 234 | fake.removeUnusedImportFromPackageReturnsOnCall = make(map[int]struct { 235 | result1 error 236 | }) 237 | } 238 | fake.removeUnusedImportFromPackageReturnsOnCall[i] = struct { 239 | result1 error 240 | }{result1} 241 | } 242 | 243 | func (fake *FakeImportsGroomer) Invocations() map[string][][]interface{} { 244 | fake.invocationsMutex.RLock() 245 | defer fake.invocationsMutex.RUnlock() 246 | fake.removeUnusedImportFromDirectoryMutex.RLock() 247 | defer fake.removeUnusedImportFromDirectoryMutex.RUnlock() 248 | fake.removeUnusedImportFromFileMutex.RLock() 249 | defer fake.removeUnusedImportFromFileMutex.RUnlock() 250 | fake.removeUnusedImportFromPackageMutex.RLock() 251 | defer fake.removeUnusedImportFromPackageMutex.RUnlock() 252 | copiedInvocations := map[string][][]interface{}{} 253 | for key, value := range fake.invocations { 254 | copiedInvocations[key] = value 255 | } 256 | return copiedInvocations 257 | } 258 | 259 | func (fake *FakeImportsGroomer) recordInvocation(key string, args []interface{}) { 260 | fake.invocationsMutex.Lock() 261 | defer fake.invocationsMutex.Unlock() 262 | if fake.invocations == nil { 263 | fake.invocations = map[string][][]interface{}{} 264 | } 265 | if fake.invocations[key] == nil { 266 | fake.invocations[key] = [][]interface{}{} 267 | } 268 | fake.invocations[key] = append(fake.invocations[key], args) 269 | } 270 | 271 | var _ tracing.ImportsGroomer = new(FakeImportsGroomer) 272 | -------------------------------------------------------------------------------- /tracing/unused_imports.go: -------------------------------------------------------------------------------- 1 | package tracing 2 | 3 | import ( 4 | "fmt" 5 | "github.com/dave/dst/decorator" 6 | "go/ast" 7 | "go/parser" 8 | "go/token" 9 | "golang.org/x/tools/go/ast/astutil" 10 | "io" 11 | "os" 12 | ) 13 | 14 | type importsGroomer struct { 15 | } 16 | 17 | func NewImportsGroomer() ImportsGroomer { 18 | return &importsGroomer{} 19 | } 20 | 21 | func (ig *importsGroomer) RemoveUnusedImportFromDirectory(path string, importsToRemove map[string]string) error { 22 | fset := token.NewFileSet() 23 | filter := func(info os.FileInfo) bool { 24 | return testsFilter(info) && generatedFilter(path, info) 25 | } 26 | pkgs, err := parser.ParseDir(fset, path, filter, parser.ParseComments) 27 | if err != nil { 28 | return fmt.Errorf("failed parsing go files in directory %s: %v", path, err) 29 | } 30 | 31 | for _, pkg := range pkgs { 32 | if err := ig.RemoveUnusedImportFromPackage(fset, pkg, importsToRemove); err != nil { 33 | return err 34 | } 35 | } 36 | return nil 37 | } 38 | 39 | func (ig *importsGroomer) RemoveUnusedImportFromPackage(fset *token.FileSet, pkg *ast.Package, importsToRemove map[string]string) error { 40 | for fileName, file := range pkg.Files { 41 | sourceFile, err := os.OpenFile(fileName, os.O_TRUNC|os.O_WRONLY, 0664) 42 | if err != nil { 43 | return fmt.Errorf("failed opening file %s: %v", fileName, err) 44 | } 45 | if err := ig.RemoveUnusedImportFromFile(fset, file, sourceFile, importsToRemove); err != nil { 46 | return fmt.Errorf("failed removing imports %v from file %s: %v", importsToRemove, fileName, err) 47 | } 48 | } 49 | return nil 50 | } 51 | 52 | func (ig *importsGroomer) RemoveUnusedImportFromFile(fset *token.FileSet, file *ast.File, out io.Writer, importsToRemove map[string]string) error { 53 | for importToRemove, alias := range importsToRemove { 54 | if !astutil.UsesImport(file, importToRemove) { 55 | astutil.DeleteNamedImport(fset, file, alias, importToRemove) 56 | } 57 | } 58 | // Needed because ast does not support floating comments and deletes them. 59 | // In order to preserve all comments we just pre-parse it to dst which treats them as first class citizens. 60 | f, err := decorator.DecorateFile(fset, file) 61 | if err != nil { 62 | return fmt.Errorf("failed converting file from ast to dst: %v", err) 63 | } 64 | 65 | return decorator.Fprint(out, f) 66 | } 67 | -------------------------------------------------------------------------------- /tracing/unused_imports_test.go: -------------------------------------------------------------------------------- 1 | package tracing 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "go/parser" 7 | "go/token" 8 | "io/ioutil" 9 | "os" 10 | "testing" 11 | ) 12 | 13 | func TestRemoveUnusedImportsFromFile(t *testing.T) { 14 | tests := []struct { 15 | Name string 16 | InputCode string 17 | OutputCode string 18 | }{ 19 | {Name: "RemoveUnusedImportsFromFileWithoutFunctions", InputCode: resultCodeWithoutFunction, OutputCode: codeWithoutFunction}, 20 | } 21 | 22 | for _, test := range tests { 23 | t.Run(test.Name, func(t *testing.T) { 24 | fset := token.NewFileSet() 25 | file, err := parser.ParseFile(fset, "", test.InputCode, parser.ParseComments) 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | var buff bytes.Buffer 30 | if err := NewImportsGroomer().RemoveUnusedImportFromFile(fset, file, &buff, map[string]string{"fmt": "", "runtime": "rt", "crypto/rand": ""}); err != nil { 31 | t.Fatal(err) 32 | } 33 | 34 | if buff.String() != test.OutputCode { 35 | t.Error("Assertion failed!") 36 | } 37 | }) 38 | } 39 | } 40 | 41 | func TestRemoveUnusedImportsFromDirectory(t *testing.T) { 42 | if err := os.Mkdir("test", 0777); err != nil { 43 | t.Fatal(err) 44 | } 45 | defer func() { 46 | if err := os.RemoveAll("test"); err != nil { 47 | t.Fatal(err) 48 | } 49 | }() 50 | 51 | filePairs := []struct { 52 | InputCode string 53 | OutputCode string 54 | }{ 55 | {InputCode: resultCodeWithoutFunction, OutputCode: codeWithoutFunction}, 56 | } 57 | 58 | i := 0 59 | for _, filePair := range filePairs { 60 | if err := ioutil.WriteFile(fmt.Sprintf("test/test%d.go", i), []byte(filePair.InputCode), 0777); err != nil { 61 | t.Fatal(err) 62 | } 63 | i++ 64 | } 65 | 66 | if err := NewImportsGroomer().RemoveUnusedImportFromDirectory("test", map[string]string{"fmt": "", "runtime": "rt", "crypto/rand": ""}); err != nil { 67 | t.Fatal(err) 68 | } 69 | 70 | i = 0 71 | for _, filePair := range filePairs { 72 | data, err := ioutil.ReadFile(fmt.Sprintf("test/test%d.go", i)) 73 | if err != nil { 74 | t.Fatal(err) 75 | } 76 | if string(data) != filePair.OutputCode { 77 | t.Error("Assertion failed!") 78 | } 79 | i++ 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /tracing/util.go: -------------------------------------------------------------------------------- 1 | package tracing 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "github.com/dave/dst" 7 | "go/token" 8 | "os" 9 | "strings" 10 | ) 11 | 12 | // Filter excluding go test files from directory 13 | func testsFilter(info os.FileInfo) bool { 14 | return !strings.HasSuffix(info.Name(), "_test.go") 15 | } 16 | 17 | // Filter excluding generated go files from directory. 18 | // Generated file is considered a file which matches one of the following: 19 | // 1. The name of the file contains "generated" 20 | // 2. First line of the file contains "generated" or "GENERATED" 21 | func generatedFilter(path string, info os.FileInfo) bool { 22 | if strings.Contains(info.Name(), "generated") { 23 | return false 24 | } 25 | 26 | f, err := os.Open(path + "/" + info.Name()) 27 | if err != nil { 28 | panic(fmt.Sprintf("Failed opening file %s: %v", path+"/"+info.Name(), err)) 29 | } 30 | defer f.Close() 31 | 32 | scanner := bufio.NewScanner(f) 33 | scanner.Scan() 34 | line := scanner.Text() 35 | 36 | if strings.Contains(line, "generated") || strings.Contains(line, "GENERATED") { 37 | return false 38 | } 39 | return true 40 | } 41 | 42 | // Return dst expresion like: fmt.Printf(args...) 43 | func newPrintExprWithArgs(args []dst.Expr) *dst.CallExpr { 44 | return &dst.CallExpr{ 45 | Fun: &dst.SelectorExpr{ 46 | X: &dst.Ident{Name: "fmt"}, 47 | Sel: &dst.Ident{Name: "Printf"}, 48 | }, 49 | Args: args, 50 | } 51 | } 52 | 53 | func buildEnteringFunctionArgs(f *dst.FuncDecl) []dst.Expr { 54 | var enteringStringFormat = "Entering function %s called by %s" 55 | args := []dst.Expr{ 56 | &dst.BasicLit{ 57 | Kind: token.STRING, 58 | Value: funcNameVarName, 59 | }, 60 | &dst.BasicLit{ 61 | Kind: token.STRING, 62 | Value: callerFuncNameVarName, 63 | }, 64 | } 65 | 66 | if len(f.Type.Params.List) > 0 { 67 | enteringStringFormat += " with args" 68 | 69 | for _, param := range f.Type.Params.List { 70 | enteringStringFormat += " (%v)" 71 | args = append(args, &dst.BasicLit{ 72 | Kind: token.STRING, 73 | Value: param.Names[0].Name, 74 | }) 75 | } 76 | } 77 | args = append(args, &dst.BasicLit{ 78 | Kind: token.STRING, 79 | Value: callIDVarName, 80 | }) 81 | args = append([]dst.Expr{ 82 | &dst.BasicLit{ 83 | Kind: token.STRING, 84 | Value: `"` + enteringStringFormat + `; callID=%s\n"`, 85 | }, 86 | }, args...) 87 | 88 | return args 89 | } 90 | 91 | func buildExitFunctionArgs() []dst.Expr { 92 | var exitingStringFormat = "Exiting function %s called by %s; callID=%s" 93 | return []dst.Expr{ 94 | &dst.BasicLit{ 95 | Kind: token.STRING, 96 | Value: `"` + exitingStringFormat + `\n"`, 97 | }, 98 | &dst.BasicLit{ 99 | Kind: token.STRING, 100 | Value: funcNameVarName, 101 | }, 102 | &dst.BasicLit{ 103 | Kind: token.STRING, 104 | Value: callerFuncNameVarName, 105 | }, 106 | &dst.BasicLit{ 107 | Kind: token.STRING, 108 | Value: callIDVarName, 109 | }, 110 | } 111 | } 112 | 113 | // Return dst statement like: varName := "value" 114 | func newAssignStmt(varName, value string) *dst.AssignStmt { 115 | return &dst.AssignStmt{ 116 | Lhs: []dst.Expr{ 117 | &dst.Ident{ 118 | Name: varName, 119 | }, 120 | }, 121 | Tok: token.DEFINE, 122 | Rhs: []dst.Expr{ 123 | &dst.BasicLit{ 124 | Kind: token.STRING, 125 | Value: `"` + value + `"`, 126 | }, 127 | }, 128 | } 129 | } 130 | 131 | /* Return dst statement like: 132 | if funcPcVarName, _, _, ok := runtime.Caller(funcIndex); ok { 133 | funcNameVarName = runtime.FuncForPC(funcPcVarName).Name() 134 | } 135 | */ 136 | func newGetFuncNameIfStatement(funcIndex, funcPcVarName, funcNameVarName string) *dst.IfStmt { 137 | return &dst.IfStmt{ 138 | Init: &dst.AssignStmt{ 139 | Lhs: []dst.Expr{ 140 | &dst.Ident{ 141 | Name: funcPcVarName, 142 | }, 143 | &dst.Ident{ 144 | Name: "_", 145 | }, 146 | &dst.Ident{ 147 | Name: "_", 148 | }, 149 | &dst.Ident{ 150 | Name: "ok", 151 | }, 152 | }, 153 | Tok: token.DEFINE, 154 | Rhs: []dst.Expr{ 155 | &dst.CallExpr{ 156 | Fun: &dst.SelectorExpr{ 157 | X: &dst.Ident{ 158 | Name: "rt", 159 | }, 160 | Sel: &dst.Ident{ 161 | Name: "Caller", 162 | }, 163 | }, 164 | Args: []dst.Expr{ 165 | &dst.BasicLit{ 166 | Kind: token.INT, 167 | Value: funcIndex, 168 | }, 169 | }, 170 | }, 171 | }, 172 | }, 173 | Cond: &dst.Ident{ 174 | Name: "ok", 175 | }, 176 | Body: &dst.BlockStmt{ 177 | List: []dst.Stmt{ 178 | &dst.AssignStmt{ 179 | Lhs: []dst.Expr{ 180 | &dst.Ident{ 181 | Name: funcNameVarName, 182 | }, 183 | }, 184 | Tok: token.ASSIGN, 185 | Rhs: []dst.Expr{ 186 | &dst.CallExpr{ 187 | Fun: &dst.SelectorExpr{ 188 | X: &dst.CallExpr{ 189 | Fun: &dst.SelectorExpr{ 190 | X: &dst.Ident{ 191 | Name: "rt", 192 | }, 193 | Sel: &dst.Ident{ 194 | Name: "FuncForPC", 195 | }, 196 | }, 197 | Args: []dst.Expr{ 198 | &dst.Ident{ 199 | Name: funcPcVarName, 200 | }, 201 | }, 202 | }, 203 | Sel: &dst.Ident{ 204 | Name: "Name", 205 | }, 206 | }, 207 | }, 208 | }, 209 | }, 210 | }, 211 | }, 212 | } 213 | } 214 | 215 | // Returns dst statement like: 216 | // idBytes := make([]byte, 16) 217 | func newMakeByteSliceStmt() *dst.AssignStmt { 218 | return &dst.AssignStmt{ 219 | Lhs: []dst.Expr{ 220 | &dst.Ident{ 221 | Name: "idBytes", 222 | }, 223 | }, 224 | Tok: token.DEFINE, 225 | Rhs: []dst.Expr{ 226 | &dst.CallExpr{ 227 | Fun: &dst.Ident{ 228 | Name: "make", 229 | }, 230 | Args: []dst.Expr{ 231 | &dst.ArrayType{ 232 | Elt: &dst.Ident{ 233 | Name: "byte", 234 | }, 235 | }, 236 | &dst.BasicLit{ 237 | Kind: token.INT, 238 | Value: "16", 239 | }, 240 | }, 241 | }, 242 | }, 243 | } 244 | } 245 | 246 | // Returns dst statement like: 247 | // _, _ = rand.Read(idBytes) 248 | func newRandReadStmt() *dst.AssignStmt { 249 | return &dst.AssignStmt{ 250 | Lhs: []dst.Expr{ 251 | &dst.Ident{ 252 | Name: "_", 253 | }, 254 | &dst.Ident{ 255 | Name: "_", 256 | }, 257 | }, 258 | Tok: token.ASSIGN, 259 | Rhs: []dst.Expr{ 260 | &dst.CallExpr{ 261 | Fun: &dst.SelectorExpr{ 262 | X: &dst.Ident{ 263 | Name: "rand", 264 | }, 265 | Sel: &dst.Ident{ 266 | Name: "Read", 267 | }, 268 | }, 269 | Args: []dst.Expr{ 270 | &dst.Ident{ 271 | Name: "idBytes", 272 | }, 273 | }, 274 | }, 275 | }, 276 | } 277 | } 278 | 279 | // Returns dst statement like: 280 | // callID := fmt.Sprintf("%x-%x-%x-%x-%x", idBytes[0:4], idBytes[4:6], idBytes[6:8], idBytes[8:10], idBytes[10:]) 281 | func newParseUUIDFromByteSliceStmt(callIDVarName string) *dst.AssignStmt { 282 | return &dst.AssignStmt{ 283 | Lhs: []dst.Expr{ 284 | &dst.Ident{ 285 | Name: callIDVarName, 286 | }, 287 | }, 288 | Tok: token.DEFINE, 289 | Rhs: []dst.Expr{ 290 | &dst.CallExpr{ 291 | Fun: &dst.SelectorExpr{ 292 | X: &dst.Ident{ 293 | Name: "fmt", 294 | }, 295 | Sel: &dst.Ident{ 296 | Name: "Sprintf", 297 | }, 298 | }, 299 | Args: []dst.Expr{ 300 | &dst.BasicLit{ 301 | Kind: token.STRING, 302 | Value: "\"%x-%x-%x-%x-%x\"", 303 | }, 304 | &dst.SliceExpr{ 305 | X: &dst.Ident{ 306 | Name: "idBytes", 307 | }, 308 | Low: &dst.BasicLit{ 309 | Kind: token.INT, 310 | Value: "0", 311 | }, 312 | High: &dst.BasicLit{ 313 | Kind: token.INT, 314 | Value: "4", 315 | }, 316 | Slice3: false, 317 | }, 318 | &dst.SliceExpr{ 319 | X: &dst.Ident{ 320 | Name: "idBytes", 321 | }, 322 | Low: &dst.BasicLit{ 323 | Kind: token.INT, 324 | Value: "4", 325 | }, 326 | High: &dst.BasicLit{ 327 | Kind: token.INT, 328 | Value: "6", 329 | }, 330 | Slice3: false, 331 | }, 332 | &dst.SliceExpr{ 333 | X: &dst.Ident{ 334 | Name: "idBytes", 335 | }, 336 | Low: &dst.BasicLit{ 337 | Kind: token.INT, 338 | Value: "6", 339 | }, 340 | High: &dst.BasicLit{ 341 | Kind: token.INT, 342 | Value: "8", 343 | }, 344 | Slice3: false, 345 | }, 346 | &dst.SliceExpr{ 347 | X: &dst.Ident{ 348 | Name: "idBytes", 349 | }, 350 | Low: &dst.BasicLit{ 351 | Kind: token.INT, 352 | Value: "8", 353 | }, 354 | High: &dst.BasicLit{ 355 | Kind: token.INT, 356 | Value: "10", 357 | }, 358 | Slice3: false, 359 | }, 360 | &dst.SliceExpr{ 361 | X: &dst.Ident{ 362 | Name: "idBytes", 363 | }, 364 | Low: &dst.BasicLit{ 365 | Kind: token.INT, 366 | Value: "10", 367 | }, 368 | Slice3: false, 369 | }, 370 | }, 371 | }, 372 | }, 373 | } 374 | } 375 | -------------------------------------------------------------------------------- /vis/vis.go: -------------------------------------------------------------------------------- 1 | package vis 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/DimitarPetrov/printracer/parser" 7 | "html/template" 8 | "math" 9 | "os" 10 | ) 11 | 12 | const reportTemplate = ` 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | 34 |Calls
42 |# | 46 |Arguments | 47 |Call ID | 48 |
---|---|---|
{{ inc $i }} | 54 |
55 |
56 | |
57 |
58 |
59 | |
60 |