diff --git a/README.md b/README.md index 3179aaa..575c13a 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,6 @@ Let's say you have a simple `main.go` file in the current working directory with ```go package main -import "fmt" - func test(i int, b bool) int { if b { return i @@ -34,8 +32,7 @@ func test(i int, b bool) int { } func main() { - i := test(2, false) - fmt.Println(i) + _ = test(2, false) } ``` @@ -48,11 +45,29 @@ The file will be modified like the following: ```go package main -import "fmt" +import ( + "crypto/rand" + "fmt" + rt "runtime" +) func test(i int, b bool) int { - fmt.Printf("Entering function test with args (%v) (%v)\n", i, b) - defer fmt.Printf("Exiting function test\n") + + /* prinTracer */ + funcName := "test" + caller := "unknown" + if funcPC, _, _, ok := rt.Caller(0); ok { + funcName = rt.FuncForPC(funcPC).Name() + } + if callerPC, _, _, ok := rt.Caller(1); ok { + caller = rt.FuncForPC(callerPC).Name() + } + idBytes := make([]byte, 16) + _, _ = rand.Read(idBytes) + callID := fmt.Sprintf("%x-%x-%x-%x-%x", idBytes[0:4], idBytes[4:6], idBytes[6:8], idBytes[8:10], idBytes[10:]) + fmt.Printf("Entering function %s called by %s with args (%v) (%v); callID=%s\n", funcName, caller, i, b, callID) + defer fmt.Printf("Exiting function %s called by %s; callID=%s\n", funcName, caller, callID) /* prinTracer */ + if b { return i } @@ -60,18 +75,42 @@ func test(i int, b bool) int { } func main() { - fmt.Printf("Entering function main\n") - defer fmt.Printf("Exiting function main\n") - i := test(2, false) - fmt.Println(i) + + /* prinTracer */ + funcName := "main" + caller := "unknown" + if funcPC, _, _, ok := rt.Caller(0); ok { + funcName = rt.FuncForPC(funcPC).Name() + } + if callerPC, _, _, ok := rt.Caller(1); ok { + caller = rt.FuncForPC(callerPC).Name() + } + idBytes := make([]byte, 16) + _, _ = rand.Read(idBytes) + callID := fmt.Sprintf("%x-%x-%x-%x-%x", idBytes[0:4], idBytes[4:6], idBytes[6:8], idBytes[8:10], idBytes[10:]) + fmt.Printf("Entering function %s called by %s; callID=%s\n", funcName, caller, callID) + defer fmt.Printf("Exiting function %s called by %s; callID=%s\n", funcName, caller, callID) /* prinTracer */ + + _ = test(2, false) } ``` +When running the instrumented file above the output (so called trace) will be as follows: +``` +Entering function main.main called by runtime.main; callID=0308fc13-5b30-5871-9101-b84e055a9565 +Entering function main.test called by main.main with args (2) (false); callID=1a3feff5-844b-039c-6d20-307d52002ce8 +Exiting function main.test called by main.main; callID=1a3feff5-844b-039c-6d20-307d52002ce8 +Exiting function main.main called by runtime.main; callID=0308fc13-5b30-5871-9101-b84e055a9565 +``` You can also easily revert all the changes done by `printracer` by just executing: ``` printracer revert ``` +> 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. + +> 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. +You also can use it to signal that a particular function should not be instrumented. ### Visualization 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. @@ -79,14 +118,14 @@ That's where visualization comes to rescue. For example let's say you have captured the following trace and saved it to the file **trace.txt**: ```text -Entering function main -Entering function foo with args (1) (true) -Entering function bar with args (test string) -Entering function baz -Exiting function baz -Exiting function bar -Exiting function foo -Exiting function main +Entering function main.main called by runtime.main; callID=ec57b80b-6898-75cc-1dea-e623e7ac26c9 +Entering function main.foo called by main.main with args (5) (false); callID=351b3edb-7ad3-2f88-1a9b-488debf800cc +Entering function main.bar called by main.foo with args (test string); callID=1e3e0e73-e4f1-b3f9-6bf5-e0aa15ddd6d1 +Entering function main.baz called by main.bar; callID=e1e79e3b-d89f-6e4e-e0bf-eea54db5b569 +Exiting function main.baz called by main.bar; callID=e1e79e3b-d89f-6e4e-e0bf-eea54db5b569 +Exiting function main.bar called by main.foo; callID=1e3e0e73-e4f1-b3f9-6bf5-e0aa15ddd6d1 +Exiting function main.foo called by main.main; callID=351b3edb-7ad3-2f88-1a9b-488debf800cc +Exiting function main.main called by runtime.main; callID=ec57b80b-6898-75cc-1dea-e623e7ac26c9 ``` In practice this would be much more complicated but it is enough for the sake of demonstration. @@ -108,9 +147,11 @@ That's where `--depth (-d)` and `--func (-f)` flags comes to rescue. - `--depth` flag controls how deep in the invocation graph you want your visualization to go. - `--func` flag controls which function to be the starting point of the visualization. +> 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! + So if you execute the following command with the trace of the previous example: ``` -printracer visualize trace.txt --depth 2 --func foo +printracer visualize trace.txt --depth 2 --func main.foo ``` A diagram like this will be generated for you: diff --git a/cmd/apply.go b/cmd/apply.go index eaac4bb..c22c1c8 100644 --- a/cmd/apply.go +++ b/cmd/apply.go @@ -41,6 +41,6 @@ func (ac *ApplyCmd) Run() error { if err != nil { return err } - return ac.importsGroomer.RemoveUnusedImportFromDirectory(path, "fmt") + return ac.importsGroomer.RemoveUnusedImportFromDirectory(path, map[string]string{"fmt": "", "runtime": "rt", "rand": ""}) // TODO: flag for import aliases }) } diff --git a/cmd/revert.go b/cmd/revert.go index 274c59f..a1bee42 100644 --- a/cmd/revert.go +++ b/cmd/revert.go @@ -41,6 +41,6 @@ func (rc *RevertCmd) Run() error { if err != nil { return err } - return rc.importsGroomer.RemoveUnusedImportFromDirectory(path, "fmt") + return rc.importsGroomer.RemoveUnusedImportFromDirectory(path, map[string]string{"fmt": "", "runtime": "rt", "crypto/rand": ""}) // TODO: flag for import aliases }) } diff --git a/cmd/visualize.go b/cmd/visualize.go index 64d9195..46cc843 100644 --- a/cmd/visualize.go +++ b/cmd/visualize.go @@ -39,8 +39,8 @@ func (vc *VisualizeCmd) Prepare() *cobra.Command { } result.Flags().StringVarP(&vc.outputFile, "output", "o", "calls", "name of the resulting html file when visualizing") - result.Flags().IntVarP(&vc.maxDepth, "depth", "d", math.MaxInt32, "maximum depth in call graph") - result.Flags().StringVarP(&vc.startingFunc, "func", "f", "", "name of the starting function in the visualization (the root of the diagram)") + 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!") + 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!") return result } diff --git a/examples/example1.png b/examples/example1.png index ea356a3..142e656 100644 Binary files a/examples/example1.png and b/examples/example1.png differ diff --git a/examples/example2.png b/examples/example2.png index d2724c3..4ea8c69 100644 Binary files a/examples/example2.png and b/examples/example2.png differ diff --git a/parser/parser.go b/parser/parser.go index b149a53..cbfcea5 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -19,24 +19,46 @@ const ( ) type FuncEvent interface { - FuncName() string + GetCaller() string + GetCallee() string + GetCallID() string } type InvocationEvent struct { - Name string - Args string + Caller string + Callee string + CallID string + Args string } -func (ie *InvocationEvent) FuncName() string { - return ie.Name +func (ie *InvocationEvent) GetCaller() string { + return ie.Caller +} + +func (ie *InvocationEvent) GetCallee() string { + return ie.Callee +} + +func (ie *InvocationEvent) GetCallID() string { + return ie.CallID } type ReturningEvent struct { - Name string + Caller string + Callee string + CallID string +} + +func (re *ReturningEvent) GetCaller() string { + return re.Caller +} + +func (re *ReturningEvent) GetCallee() string { + return re.Callee } -func (ie *ReturningEvent) FuncName() string { - return ie.Name +func (re *ReturningEvent) GetCallID() string { + return re.CallID } type parser struct { @@ -51,18 +73,26 @@ func (p *parser) Parse(in io.Reader) ([]FuncEvent, error) { scanner := bufio.NewScanner(in) for scanner.Scan() { row := scanner.Text() - if strings.HasPrefix(row, "Entering function") { - words := strings.Split(row, " ") + lastSemicolon := strings.LastIndex(row, ";") + msg := row[:lastSemicolon] + secondHalf := row[lastSemicolon+1:] + callID := strings.Split(secondHalf, "=")[1] + if strings.HasPrefix(msg, "Entering function") { + words := strings.Split(msg, " ") events = append(events, &InvocationEvent{ - Name: words[2], - Args: strings.Join(words[3:], " "), + Callee: normalizeFuncName(words[2]), + Caller: normalizeFuncName(words[5]), + Args: strings.Join(words[6:], " "), + CallID: callID, }) } - if strings.HasPrefix(row, "Exiting function") { - split := strings.Split(row, " ") + if strings.HasPrefix(msg, "Exiting function") { + words := strings.Split(msg, " ") events = append(events, &ReturningEvent{ - Name: split[2], + Callee: normalizeFuncName(words[2]), + Caller: normalizeFuncName(words[5]), + CallID: callID, }) } } @@ -72,3 +102,7 @@ func (p *parser) Parse(in io.Reader) ([]FuncEvent, error) { return events, nil } + +func normalizeFuncName(funcName string) string { + return funcName[strings.LastIndex(funcName, "/")+1:] +} diff --git a/parser/parser_test.go b/parser/parser_test.go index 91d2f9a..1f2ff66 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -7,34 +7,58 @@ import ( ) func TestParser_Parse(t *testing.T) { - input := `Entering function main -Entering function foo with args (1) (true) -Entering function bar with args (test string) -Entering function baz -Exiting function baz -Exiting function bar -Exiting function foo -Exiting function main` + input := `Entering function main.main called by runtime.main; callID=1d8ca74e-c860-8a75-fc36-fe6d34350f0c +Entering function main.foo called by main.main with args (5) (false); callID=973355a9-2ec6-095c-9137-7a1081ac0a5f +Entering function main.bar called by main.foo with args (test string); callID=6c294dfd-4c6a-39b1-474e-314bee73f514 +Entering function main.baz called by main.bar; callID=a019a297-0a6e-a792-0e3f-23c33a44622f +Exiting function main.baz called by main.bar; callID=a019a297-0a6e-a792-0e3f-23c33a44622f +Exiting function main.bar called by main.foo; callID=6c294dfd-4c6a-39b1-474e-314bee73f514 +Exiting function main.foo called by main.main; callID=973355a9-2ec6-095c-9137-7a1081ac0a5f +Exiting function main.main called by runtime.main; callID=1d8ca74e-c860-8a75-fc36-fe6d34350f0c` expected := []FuncEvent{ &InvocationEvent{ - Name: "main", + Caller: "runtime.main", + Callee: "main.main", + CallID: "1d8ca74e-c860-8a75-fc36-fe6d34350f0c", }, &InvocationEvent{ - Name: "foo", - Args: "with args (1) (true)", + Caller: "main.main", + Callee: "main.foo", + CallID: "973355a9-2ec6-095c-9137-7a1081ac0a5f", + Args: "with args (5) (false)", }, &InvocationEvent{ - Name: "bar", - Args: "with args (test string)", + Caller: "main.foo", + Callee: "main.bar", + CallID: "6c294dfd-4c6a-39b1-474e-314bee73f514", + Args: "with args (test string)", }, &InvocationEvent{ - Name: "baz", + Caller: "main.bar", + Callee: "main.baz", + CallID: "a019a297-0a6e-a792-0e3f-23c33a44622f", + }, + &ReturningEvent{ + Caller: "main.bar", + Callee: "main.baz", + CallID: "a019a297-0a6e-a792-0e3f-23c33a44622f", + }, + &ReturningEvent{ + Caller: "main.foo", + Callee: "main.bar", + CallID: "6c294dfd-4c6a-39b1-474e-314bee73f514", + }, + &ReturningEvent{ + Caller: "main.main", + Callee: "main.foo", + CallID: "973355a9-2ec6-095c-9137-7a1081ac0a5f", + }, + &ReturningEvent{ + Caller: "runtime.main", + Callee: "main.main", + CallID: "1d8ca74e-c860-8a75-fc36-fe6d34350f0c", }, - &ReturningEvent{Name: "baz"}, - &ReturningEvent{Name: "bar"}, - &ReturningEvent{Name: "foo"}, - &ReturningEvent{Name: "main"}, } actual, err := NewParser().Parse(bytes.NewBufferString(input)) diff --git a/tracing/deinstrument.go b/tracing/deinstrument.go index 4f6bc8a..5a6454b 100644 --- a/tracing/deinstrument.go +++ b/tracing/deinstrument.go @@ -9,7 +9,7 @@ import ( "go/token" "io" "os" - "strings" + "reflect" ) type codeDeinstrumenter struct { @@ -60,28 +60,16 @@ func (cd *codeDeinstrumenter) DeinstrumentFile(fset *token.FileSet, file *ast.Fi dst.Inspect(f, func(n dst.Node) bool { switch t := n.(type) { case *dst.FuncDecl: - if len(t.Body.List) > 1 { - stmt1, ok1 := t.Body.List[0].(*dst.ExprStmt) - stmt2, ok2 := t.Body.List[1].(*dst.DeferStmt) - if ok1 && ok2 { - expr1, ok := stmt1.X.(*dst.CallExpr) - if ok { - selExpr1, ok1 := expr1.Fun.(*dst.SelectorExpr) - selExpr2, ok2 := stmt2.Call.Fun.(*dst.SelectorExpr) - if ok1 && ok2 { - package1, ok1 := selExpr1.X.(*dst.Ident) - package2, ok2 := selExpr2.X.(*dst.Ident) - if ok1 && ok2 && package1.Name == "fmt" && package2.Name == "fmt" && - selExpr1.Sel.Name == "Printf" && selExpr2.Sel.Name == "Printf" { + if len(t.Body.List) >= instrumentationStmtsCount { + firstStmntDecorations := t.Body.List[0].Decorations().Start.All() + secondStmntDecorations := t.Body.List[instrumentationStmtsCount-1].Decorations().End.All() + if len(firstStmntDecorations) > 0 && firstStmntDecorations[0] == printracerCommentWatermark && + len(secondStmntDecorations) > 0 && secondStmntDecorations[0] == printracerCommentWatermark { - expr1Arg, ok1 := expr1.Args[0].(*dst.BasicLit) - expr2Arg, ok2 := stmt2.Call.Args[0].(*dst.BasicLit) - if ok1 && ok2 && expr1Arg.Kind == token.STRING && expr2Arg.Kind == token.STRING && - strings.Contains(expr1Arg.Value, fmt.Sprintf("Entering function %s", t.Name)) && - strings.Contains(expr2Arg.Value, fmt.Sprintf("Exiting function %s", t.Name)) { - t.Body.List = t.Body.List[2:] - } - } + if checkInstrumentationStatementsIntegrity(t) { + t.Body.List = t.Body.List[instrumentationStmtsCount:] + if len(t.Body.List) > 0 { + t.Body.List[0].Decorations().Before = dst.None } } } @@ -92,3 +80,132 @@ func (cd *codeDeinstrumenter) DeinstrumentFile(fset *token.FileSet, file *ast.Fi return decorator.Fprint(out, f) } + +func checkInstrumentationStatementsIntegrity(f *dst.FuncDecl) bool { + stmts := f.Body.List + instrumentationStmts := buildInstrumentationStmts(f) + + for i := 0; i < instrumentationStmtsCount; i++ { + if !equalStmt(stmts[i], instrumentationStmts[i]) { + return false + } + } + return true +} + +func equalStmt(stmt1, stmt2 dst.Stmt) bool { + switch t := stmt1.(type) { + case *dst.AssignStmt: + instStmt, ok := stmt2.(*dst.AssignStmt) + if !ok { + return false + } + if !(equalExprSlice(t.Lhs, instStmt.Lhs) && equalExprSlice(t.Rhs, instStmt.Rhs) && reflect.DeepEqual(t.Tok, instStmt.Tok)) { + return false + } + return true + case *dst.IfStmt: + instStmt, ok := stmt2.(*dst.IfStmt) + if !ok { + return false + } + if !(equalStmt(t.Init, instStmt.Init) && equalExpr(t.Cond, instStmt.Cond) && equalStmt(t.Body, instStmt.Body) && equalStmt(t.Else, instStmt.Else)) { + return false + } + return true + case *dst.ExprStmt: + instStmt, ok := stmt2.(*dst.ExprStmt) + if !ok { + return false + } + if !(equalExpr(t.X, instStmt.X)) { + return false + } + return true + case *dst.DeferStmt: + instStmt, ok := stmt2.(*dst.DeferStmt) + if !ok { + return false + } + if !(equalExpr(t.Call, instStmt.Call)) { + return false + } + return true + case *dst.BlockStmt: + instStmt, ok := stmt2.(*dst.BlockStmt) + if !ok { + return false + } + if len(t.List) != len(instStmt.List) || t.RbraceHasNoPos != instStmt.RbraceHasNoPos { + return false + } + for i, stmt1 := range t.List { + if !equalStmt(stmt1, instStmt.List[i]) { + return false + } + } + return true + } + return reflect.DeepEqual(stmt1, stmt2) +} + +func equalExprSlice(exprSlice1, exprSlice2 []dst.Expr) bool { + if len(exprSlice1) != len(exprSlice2) { + return false + } + for i, expr1 := range exprSlice1 { + if !equalExpr(expr1, exprSlice2[i]) { + return false + } + } + return true +} + +func equalExpr(expr1, expr2 dst.Expr) bool { + switch t := expr1.(type) { + case *dst.Ident: + instExpr, ok := expr2.(*dst.Ident) + if !ok { + instExpr, ok := expr2.(*dst.BasicLit) + if !ok { + return false + } + return t.Name == instExpr.Value + } + return t.Name == instExpr.Name && t.Path == instExpr.Path + case *dst.CallExpr: + instExpr, ok := expr2.(*dst.CallExpr) + if !ok { + return false + } + if !(equalExprSlice(t.Args, instExpr.Args) && equalExpr(t.Fun, instExpr.Fun)) { + return false + } + return true + case *dst.SelectorExpr: + instExpr, ok := expr2.(*dst.SelectorExpr) + if !ok { + return false + } + if !(equalExpr(t.X, instExpr.X) && equalExpr(t.Sel, instExpr.Sel)) { + return false + } + return true + case *dst.SliceExpr: + instExpr, ok := expr2.(*dst.SliceExpr) + if !ok { + return false + } + 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)) { + return false + } + return true + case *dst.BasicLit: + instExpr, ok := expr2.(*dst.BasicLit) + if !ok { + return false + } + return t.Value == instExpr.Value && t.Kind == instExpr.Kind + } + return reflect.DeepEqual(expr1, expr2) +} diff --git a/tracing/deinstrument_test.go b/tracing/deinstrument_test.go index 4418c95..7eb77f8 100644 --- a/tracing/deinstrument_test.go +++ b/tracing/deinstrument_test.go @@ -22,6 +22,7 @@ func TestDeinstrumentFile(t *testing.T) { {Name: "DeinstrumentFileWithoutFmtImport", InputCode: resultCodeWithImportsWithoutFmt, OutputCode: codeWithImportsWithoutFmt}, {Name: "DeinstrumentFileWithoutFunctions", InputCode: resultCodeWithoutFunction, OutputCode: codeWithoutFunction}, {Name: "DeinstrumentFileWithoutPreviousInstrumentation", InputCode: codeWithMultipleImports, OutputCode: codeWithMultipleImports}, + {Name: "DeinstrumentFileDoesNotChangeManuallyEditedFunctions", InputCode: editedResultCodeWithoutImports, OutputCode: editedResultCodeWithoutImports}, } for _, test := range tests { @@ -42,12 +43,12 @@ func TestDeinstrumentFile(t *testing.T) { } var buff2 bytes.Buffer - if err := NewImportsGroomer().RemoveUnusedImportFromFile(fset, file, &buff2, "fmt"); err != nil { + if err := NewImportsGroomer().RemoveUnusedImportFromFile(fset, file, &buff2, map[string]string{"fmt": "", "runtime": "rt", "crypto/rand": ""}); err != nil { t.Fatal(err) } if buff2.String() != test.OutputCode { - t.Error("Assertion failed!") + t.Errorf("Assertion failed! Expected %s god %s", test.OutputCode, buff2.String()) } }) } @@ -72,6 +73,7 @@ func TestDeinstrumentDirectory(t *testing.T) { {InputCode: resultCodeWithMultipleImports, OutputCode: codeWithMultipleImports}, {InputCode: resultCodeWithImportsWithoutFmt, OutputCode: codeWithImportsWithoutFmt}, {InputCode: resultCodeWithoutFunction, OutputCode: codeWithoutFunction}, + {InputCode: editedResultCodeWithoutImports, OutputCode: editedResultCodeWithoutImports}, } i := 0 @@ -86,7 +88,7 @@ func TestDeinstrumentDirectory(t *testing.T) { t.Fatal(err) } - if err := NewImportsGroomer().RemoveUnusedImportFromDirectory("test", "fmt"); err != nil { + if err := NewImportsGroomer().RemoveUnusedImportFromDirectory("test", map[string]string{"fmt": "", "runtime": "rt", "crypto/rand": ""}); err != nil { t.Fatal(err) } diff --git a/tracing/instrument.go b/tracing/instrument.go index 796af08..7e58748 100644 --- a/tracing/instrument.go +++ b/tracing/instrument.go @@ -12,6 +12,37 @@ import ( "os" ) +const funcNameVarName = "funcName" +const funcPCVarName = "funcPC" + +const callerFuncNameVarName = "caller" +const defaultCallerName = "unknown" +const callerFuncPCVarName = "callerPC" + +const callIDVarName = "callID" + +const printracerCommentWatermark = "/* prinTracer */" + +const instrumentationStmtsCount = 9 // Acts like a contract of how many statements instrumentation adds and deinstrumentation removes. + +func buildInstrumentationStmts(f *dst.FuncDecl) [instrumentationStmtsCount]dst.Stmt { + return [instrumentationStmtsCount]dst.Stmt{ + newAssignStmt(funcNameVarName, f.Name.Name), + newAssignStmt(callerFuncNameVarName, defaultCallerName), + newGetFuncNameIfStatement("0", funcPCVarName, funcNameVarName), + newGetFuncNameIfStatement("1", callerFuncPCVarName, callerFuncNameVarName), + newMakeByteSliceStmt(), + newRandReadStmt(), + newParseUUIDFromByteSliceStmt(callIDVarName), + &dst.ExprStmt{ + X: newPrintExprWithArgs(buildEnteringFunctionArgs(f)), + }, + &dst.DeferStmt{ + Call: newPrintExprWithArgs(buildExitFunctionArgs()), + }, + } +} + type codeInstrumenter struct { } @@ -52,6 +83,8 @@ func (ci *codeInstrumenter) InstrumentPackage(fset *token.FileSet, pkg *ast.Pack func (ci *codeInstrumenter) InstrumentFile(fset *token.FileSet, file *ast.File, out io.Writer) error { astutil.AddImport(fset, file, "fmt") + astutil.AddNamedImport(fset, file, "rt", "runtime") + astutil.AddImport(fset, file, "crypto/rand") // Needed because ast does not support floating comments and deletes them. // In order to preserve all comments we just pre-parse it to dst which treats them as first class citizens. @@ -63,40 +96,27 @@ func (ci *codeInstrumenter) InstrumentFile(fset *token.FileSet, file *ast.File, dst.Inspect(f, func(n dst.Node) bool { switch t := n.(type) { case *dst.FuncDecl: - var enteringStringFormat = fmt.Sprintf("Entering function %s", t.Name) - var exitingStringFormat = fmt.Sprintf("Exiting function %s", t.Name) + if !ci.hasInstrumentationWatermark(t) { + instrumentationStmts := buildInstrumentationStmts(t) + t.Body.List = append(instrumentationStmts[:], t.Body.List...) - var args []dst.Expr - - if len(t.Type.Params.List) > 0 { - enteringStringFormat += " with args" - - for _, param := range t.Type.Params.List { - enteringStringFormat += " (%v)" - args = append(args, &dst.BasicLit{ - Kind: token.STRING, - Value: param.Names[0].Name, - }) - } + t.Body.List[0].Decorations().Before = dst.EmptyLine + t.Body.List[0].Decorations().Start.Append(printracerCommentWatermark) + t.Body.List[instrumentationStmtsCount-1].Decorations().After = dst.EmptyLine + t.Body.List[instrumentationStmtsCount-1].Decorations().End.Append(printracerCommentWatermark) } - - args = append([]dst.Expr{ - &dst.BasicLit{ - Kind: token.STRING, - Value: `"` + enteringStringFormat + `\n"`, - }, - }, args...) - - t.Body.List = append([]dst.Stmt{ - &dst.ExprStmt{ - X: newPrintExprWithArgs(args), - }, - &dst.DeferStmt{ - Call: newPrintExprWithMessage(exitingStringFormat), - }, - }, t.Body.List...) } return true }) return decorator.Fprint(out, f) } + +func (ci *codeInstrumenter) hasInstrumentationWatermark(f *dst.FuncDecl) bool { + if len(f.Body.List) > 0 { + firstStmntDecorations := f.Body.List[0].Decorations().Start.All() + if len(firstStmntDecorations) > 0 && firstStmntDecorations[0] == printracerCommentWatermark { + return true + } + } + return false +} diff --git a/tracing/instrument_test.go b/tracing/instrument_test.go index dde2c2b..8f8ab3e 100644 --- a/tracing/instrument_test.go +++ b/tracing/instrument_test.go @@ -26,11 +26,29 @@ func main() { const resultCodeWithoutImports = `package a -import "fmt" +import ( + "crypto/rand" + "fmt" + rt "runtime" +) func test(i int, b bool) int { - fmt.Printf("Entering function test with args (%v) (%v)\n", i, b) - defer fmt.Printf("Exiting function test\n") + + /* prinTracer */ + funcName := "test" + caller := "unknown" + if funcPC, _, _, ok := rt.Caller(0); ok { + funcName = rt.FuncForPC(funcPC).Name() + } + if callerPC, _, _, ok := rt.Caller(1); ok { + caller = rt.FuncForPC(callerPC).Name() + } + idBytes := make([]byte, 16) + _, _ = rand.Read(idBytes) + callID := fmt.Sprintf("%x-%x-%x-%x-%x", idBytes[0:4], idBytes[4:6], idBytes[6:8], idBytes[8:10], idBytes[10:]) + fmt.Printf("Entering function %s called by %s with args (%v) (%v); callID=%s\n", funcName, caller, i, b, callID) + defer fmt.Printf("Exiting function %s called by %s; callID=%s\n", funcName, caller, callID) /* prinTracer */ + if b { return i } @@ -38,15 +56,83 @@ func test(i int, b bool) int { } func main() { - fmt.Printf("Entering function main\n") - defer fmt.Printf("Exiting function main\n") + + /* prinTracer */ + funcName := "main" + caller := "unknown" + if funcPC, _, _, ok := rt.Caller(0); ok { + funcName = rt.FuncForPC(funcPC).Name() + } + if callerPC, _, _, ok := rt.Caller(1); ok { + caller = rt.FuncForPC(callerPC).Name() + } + idBytes := make([]byte, 16) + _, _ = rand.Read(idBytes) + callID := fmt.Sprintf("%x-%x-%x-%x-%x", idBytes[0:4], idBytes[4:6], idBytes[6:8], idBytes[8:10], idBytes[10:]) + fmt.Printf("Entering function %s called by %s; callID=%s\n", funcName, caller, callID) + defer fmt.Printf("Exiting function %s called by %s; callID=%s\n", funcName, caller, callID) /* prinTracer */ + + i := test(2, false) +} +` + +const editedResultCodeWithoutImports = `package a + +import ( + "crypto/rand" + "fmt" + rt "runtime" +) + +func test(i int, b bool) int { + + /* prinTracer */ + funcName := "test2" + caller := "unknown2" + if funcPC, _, _, ok := rt.Caller(0); ok { + funcName = rt.FuncForPC(funcPC).Name() + } + if callerPC, _, _, ok := rt.Caller(1); ok { + caller = rt.FuncForPC(callerPC).Name() + } + fmt.Println("test") + idBytes := make([]byte, 16) + _, _ = rand.Read(idBytes) + callID := fmt.Sprintf("%x-%x-%x-%x-%x", idBytes[0:4], idBytes[4:6], idBytes[6:8], idBytes[8:10], idBytes[10:]) + fmt.Printf("Entering function %s called by %s with args (%v) (%v); callID=%s\n", funcName, caller, i, b, callID) + defer fmt.Printf("Exiting function %s called by %s; callID=%s\n", funcName, caller, callID) /* prinTracer */ + + if b { + return i + } + return 0 +} + +func main() { + + funcName := "main" + caller := "unknown" + if funcPC, _, _, ok := rt.Caller(0); ok { + funcName = rt.FuncForPC(funcPC).Name() + } + if callerPC, _, _, ok := rt.Caller(1); ok { + caller = rt.FuncForPC(callerPC).Name() + } + idBytes := make([]byte, 16) + _, _ = rand.Read(idBytes) + callID := fmt.Sprintf("%x-%x-%x-%x-%x", idBytes[0:4], idBytes[4:6], idBytes[6:8], idBytes[8:10], idBytes[10:]) + fmt.Printf("Entering function %s called by %s; callID=%s\n", funcName, caller, callID) + defer fmt.Printf("Exiting function %s called by %s; callID=%s\n", funcName, caller, callID) + i := test(2, false) } ` const codeWithFmtImport = `package a -import "fmt" +import ( + "fmt" +) func test(i int, b bool) int { if b { @@ -62,11 +148,29 @@ func main() { ` const resultCodeWithFmtImport = `package a -import "fmt" +import ( + "crypto/rand" + "fmt" + rt "runtime" +) func test(i int, b bool) int { - fmt.Printf("Entering function test with args (%v) (%v)\n", i, b) - defer fmt.Printf("Exiting function test\n") + + /* prinTracer */ + funcName := "test" + caller := "unknown" + if funcPC, _, _, ok := rt.Caller(0); ok { + funcName = rt.FuncForPC(funcPC).Name() + } + if callerPC, _, _, ok := rt.Caller(1); ok { + caller = rt.FuncForPC(callerPC).Name() + } + idBytes := make([]byte, 16) + _, _ = rand.Read(idBytes) + callID := fmt.Sprintf("%x-%x-%x-%x-%x", idBytes[0:4], idBytes[4:6], idBytes[6:8], idBytes[8:10], idBytes[10:]) + fmt.Printf("Entering function %s called by %s with args (%v) (%v); callID=%s\n", funcName, caller, i, b, callID) + defer fmt.Printf("Exiting function %s called by %s; callID=%s\n", funcName, caller, callID) /* prinTracer */ + if b { return i } @@ -74,8 +178,22 @@ func test(i int, b bool) int { } func main() { - fmt.Printf("Entering function main\n") - defer fmt.Printf("Exiting function main\n") + + /* prinTracer */ + funcName := "main" + caller := "unknown" + if funcPC, _, _, ok := rt.Caller(0); ok { + funcName = rt.FuncForPC(funcPC).Name() + } + if callerPC, _, _, ok := rt.Caller(1); ok { + caller = rt.FuncForPC(callerPC).Name() + } + idBytes := make([]byte, 16) + _, _ = rand.Read(idBytes) + callID := fmt.Sprintf("%x-%x-%x-%x-%x", idBytes[0:4], idBytes[4:6], idBytes[6:8], idBytes[8:10], idBytes[10:]) + fmt.Printf("Entering function %s called by %s; callID=%s\n", funcName, caller, callID) + defer fmt.Printf("Exiting function %s called by %s; callID=%s\n", funcName, caller, callID) /* prinTracer */ + i := test(2, false) fmt.Println(i) } @@ -104,13 +222,29 @@ func main() { const resultCodeWithMultipleImports = `package a import ( + "crypto/rand" "fmt" + rt "runtime" "strconv" ) func test(i int, b bool) int { - fmt.Printf("Entering function test with args (%v) (%v)\n", i, b) - defer fmt.Printf("Exiting function test\n") + + /* prinTracer */ + funcName := "test" + caller := "unknown" + if funcPC, _, _, ok := rt.Caller(0); ok { + funcName = rt.FuncForPC(funcPC).Name() + } + if callerPC, _, _, ok := rt.Caller(1); ok { + caller = rt.FuncForPC(callerPC).Name() + } + idBytes := make([]byte, 16) + _, _ = rand.Read(idBytes) + callID := fmt.Sprintf("%x-%x-%x-%x-%x", idBytes[0:4], idBytes[4:6], idBytes[6:8], idBytes[8:10], idBytes[10:]) + fmt.Printf("Entering function %s called by %s with args (%v) (%v); callID=%s\n", funcName, caller, i, b, callID) + defer fmt.Printf("Exiting function %s called by %s; callID=%s\n", funcName, caller, callID) /* prinTracer */ + if b { return i } @@ -118,8 +252,22 @@ func test(i int, b bool) int { } func main() { - fmt.Printf("Entering function main\n") - defer fmt.Printf("Exiting function main\n") + + /* prinTracer */ + funcName := "main" + caller := "unknown" + if funcPC, _, _, ok := rt.Caller(0); ok { + funcName = rt.FuncForPC(funcPC).Name() + } + if callerPC, _, _, ok := rt.Caller(1); ok { + caller = rt.FuncForPC(callerPC).Name() + } + idBytes := make([]byte, 16) + _, _ = rand.Read(idBytes) + callID := fmt.Sprintf("%x-%x-%x-%x-%x", idBytes[0:4], idBytes[4:6], idBytes[6:8], idBytes[8:10], idBytes[10:]) + fmt.Printf("Entering function %s called by %s; callID=%s\n", funcName, caller, callID) + defer fmt.Printf("Exiting function %s called by %s; callID=%s\n", funcName, caller, callID) /* prinTracer */ + i := test(2, false) fmt.Println(strconv.Itoa(i)) } @@ -147,13 +295,29 @@ func main() { const resultCodeWithImportsWithoutFmt = `package a import ( + "crypto/rand" "fmt" + rt "runtime" "strconv" ) func test(i int, b bool) int { - fmt.Printf("Entering function test with args (%v) (%v)\n", i, b) - defer fmt.Printf("Exiting function test\n") + + /* prinTracer */ + funcName := "test" + caller := "unknown" + if funcPC, _, _, ok := rt.Caller(0); ok { + funcName = rt.FuncForPC(funcPC).Name() + } + if callerPC, _, _, ok := rt.Caller(1); ok { + caller = rt.FuncForPC(callerPC).Name() + } + idBytes := make([]byte, 16) + _, _ = rand.Read(idBytes) + callID := fmt.Sprintf("%x-%x-%x-%x-%x", idBytes[0:4], idBytes[4:6], idBytes[6:8], idBytes[8:10], idBytes[10:]) + fmt.Printf("Entering function %s called by %s with args (%v) (%v); callID=%s\n", funcName, caller, i, b, callID) + defer fmt.Printf("Exiting function %s called by %s; callID=%s\n", funcName, caller, callID) /* prinTracer */ + if b { return i } @@ -161,13 +325,49 @@ func test(i int, b bool) int { } func main() { - fmt.Printf("Entering function main\n") - defer fmt.Printf("Exiting function main\n") + + /* prinTracer */ + funcName := "main" + caller := "unknown" + if funcPC, _, _, ok := rt.Caller(0); ok { + funcName = rt.FuncForPC(funcPC).Name() + } + if callerPC, _, _, ok := rt.Caller(1); ok { + caller = rt.FuncForPC(callerPC).Name() + } + idBytes := make([]byte, 16) + _, _ = rand.Read(idBytes) + callID := fmt.Sprintf("%x-%x-%x-%x-%x", idBytes[0:4], idBytes[4:6], idBytes[6:8], idBytes[8:10], idBytes[10:]) + fmt.Printf("Entering function %s called by %s; callID=%s\n", funcName, caller, callID) + defer fmt.Printf("Exiting function %s called by %s; callID=%s\n", funcName, caller, callID) /* prinTracer */ + i := test(2, false) s := strconv.Itoa(i) } ` +const codeWithWatermarks = `package a + +import ( + "crypto/rand" + "fmt" + rt "runtime" +) + +func test(i int, b bool) int { + /* prinTracer */ + if b { + return i + } + return 0 +} + +func main() { + /* prinTracer */ + i := test(2, false) +} +` + const codeWithoutFunction = `package a type test struct { @@ -177,7 +377,11 @@ type test struct { const resultCodeWithoutFunction = `package a -import "fmt" +import ( + "crypto/rand" + "fmt" + rt "runtime" +) type test struct { a int @@ -195,6 +399,8 @@ func TestInstrumentFile(t *testing.T) { {Name: "InstrumentFileWithMultipleImports", InputCode: codeWithMultipleImports, OutputCode: resultCodeWithMultipleImports}, {Name: "InstrumentFileWithoutFmtImport", InputCode: codeWithImportsWithoutFmt, OutputCode: resultCodeWithImportsWithoutFmt}, {Name: "InstrumentFileWithoutFunctions", InputCode: codeWithoutFunction, OutputCode: resultCodeWithoutFunction}, + {Name: "InstrumentFileDoesNotAffectAlreadyInstrumentedFiles", InputCode: resultCodeWithFmtImport, OutputCode: resultCodeWithFmtImport}, + {Name: "FunctionsWithWatermarksShouldNotBeInstrumented", InputCode: codeWithWatermarks, OutputCode: codeWithWatermarks}, } for _, test := range tests { @@ -210,7 +416,7 @@ func TestInstrumentFile(t *testing.T) { } if buff.String() != test.OutputCode { - t.Error("Assertion failed!") + t.Errorf("Assertion failed! Expected %s got %s", test.OutputCode, buff.String()) } }) } @@ -235,6 +441,7 @@ func TestInstrumentDirectory(t *testing.T) { {InputCode: codeWithMultipleImports, OutputCode: resultCodeWithMultipleImports}, {InputCode: codeWithImportsWithoutFmt, OutputCode: resultCodeWithImportsWithoutFmt}, {InputCode: codeWithoutFunction, OutputCode: resultCodeWithoutFunction}, + {InputCode: resultCodeWithFmtImport, OutputCode: resultCodeWithFmtImport}, } i := 0 diff --git a/tracing/interfaces.go b/tracing/interfaces.go index bc0438e..8e2bb24 100644 --- a/tracing/interfaces.go +++ b/tracing/interfaces.go @@ -22,7 +22,7 @@ type CodeDeinstrumenter interface { //go:generate counterfeiter . ImportsGroomer type ImportsGroomer interface { - RemoveUnusedImportFromFile(fset *token.FileSet, file *ast.File, out io.Writer, importToRemove string) error - RemoveUnusedImportFromPackage(fset *token.FileSet, pkg *ast.Package, importToRemove string) error - RemoveUnusedImportFromDirectory(path string, importToRemove string) error + RemoveUnusedImportFromFile(fset *token.FileSet, file *ast.File, out io.Writer, importsToRemove map[string]string) error + RemoveUnusedImportFromPackage(fset *token.FileSet, pkg *ast.Package, importsToRemove map[string]string) error + RemoveUnusedImportFromDirectory(path string, importsToRemove map[string]string) error } diff --git a/tracing/tracingfakes/fake_imports_groomer.go b/tracing/tracingfakes/fake_imports_groomer.go index 756fdf2..d79c07e 100644 --- a/tracing/tracingfakes/fake_imports_groomer.go +++ b/tracing/tracingfakes/fake_imports_groomer.go @@ -11,11 +11,11 @@ import ( ) type FakeImportsGroomer struct { - RemoveUnusedImportFromDirectoryStub func(string, string) error + RemoveUnusedImportFromDirectoryStub func(string, map[string]string) error removeUnusedImportFromDirectoryMutex sync.RWMutex removeUnusedImportFromDirectoryArgsForCall []struct { arg1 string - arg2 string + arg2 map[string]string } removeUnusedImportFromDirectoryReturns struct { result1 error @@ -23,13 +23,13 @@ type FakeImportsGroomer struct { removeUnusedImportFromDirectoryReturnsOnCall map[int]struct { result1 error } - RemoveUnusedImportFromFileStub func(*token.FileSet, *ast.File, io.Writer, string) error + RemoveUnusedImportFromFileStub func(*token.FileSet, *ast.File, io.Writer, map[string]string) error removeUnusedImportFromFileMutex sync.RWMutex removeUnusedImportFromFileArgsForCall []struct { arg1 *token.FileSet arg2 *ast.File arg3 io.Writer - arg4 string + arg4 map[string]string } removeUnusedImportFromFileReturns struct { result1 error @@ -37,12 +37,12 @@ type FakeImportsGroomer struct { removeUnusedImportFromFileReturnsOnCall map[int]struct { result1 error } - RemoveUnusedImportFromPackageStub func(*token.FileSet, *ast.Package, string) error + RemoveUnusedImportFromPackageStub func(*token.FileSet, *ast.Package, map[string]string) error removeUnusedImportFromPackageMutex sync.RWMutex removeUnusedImportFromPackageArgsForCall []struct { arg1 *token.FileSet arg2 *ast.Package - arg3 string + arg3 map[string]string } removeUnusedImportFromPackageReturns struct { result1 error @@ -54,12 +54,12 @@ type FakeImportsGroomer struct { invocationsMutex sync.RWMutex } -func (fake *FakeImportsGroomer) RemoveUnusedImportFromDirectory(arg1 string, arg2 string) error { +func (fake *FakeImportsGroomer) RemoveUnusedImportFromDirectory(arg1 string, arg2 map[string]string) error { fake.removeUnusedImportFromDirectoryMutex.Lock() ret, specificReturn := fake.removeUnusedImportFromDirectoryReturnsOnCall[len(fake.removeUnusedImportFromDirectoryArgsForCall)] fake.removeUnusedImportFromDirectoryArgsForCall = append(fake.removeUnusedImportFromDirectoryArgsForCall, struct { arg1 string - arg2 string + arg2 map[string]string }{arg1, arg2}) fake.recordInvocation("RemoveUnusedImportFromDirectory", []interface{}{arg1, arg2}) fake.removeUnusedImportFromDirectoryMutex.Unlock() @@ -79,13 +79,13 @@ func (fake *FakeImportsGroomer) RemoveUnusedImportFromDirectoryCallCount() int { return len(fake.removeUnusedImportFromDirectoryArgsForCall) } -func (fake *FakeImportsGroomer) RemoveUnusedImportFromDirectoryCalls(stub func(string, string) error) { +func (fake *FakeImportsGroomer) RemoveUnusedImportFromDirectoryCalls(stub func(string, map[string]string) error) { fake.removeUnusedImportFromDirectoryMutex.Lock() defer fake.removeUnusedImportFromDirectoryMutex.Unlock() fake.RemoveUnusedImportFromDirectoryStub = stub } -func (fake *FakeImportsGroomer) RemoveUnusedImportFromDirectoryArgsForCall(i int) (string, string) { +func (fake *FakeImportsGroomer) RemoveUnusedImportFromDirectoryArgsForCall(i int) (string, map[string]string) { fake.removeUnusedImportFromDirectoryMutex.RLock() defer fake.removeUnusedImportFromDirectoryMutex.RUnlock() argsForCall := fake.removeUnusedImportFromDirectoryArgsForCall[i] @@ -115,14 +115,14 @@ func (fake *FakeImportsGroomer) RemoveUnusedImportFromDirectoryReturnsOnCall(i i }{result1} } -func (fake *FakeImportsGroomer) RemoveUnusedImportFromFile(arg1 *token.FileSet, arg2 *ast.File, arg3 io.Writer, arg4 string) error { +func (fake *FakeImportsGroomer) RemoveUnusedImportFromFile(arg1 *token.FileSet, arg2 *ast.File, arg3 io.Writer, arg4 map[string]string) error { fake.removeUnusedImportFromFileMutex.Lock() ret, specificReturn := fake.removeUnusedImportFromFileReturnsOnCall[len(fake.removeUnusedImportFromFileArgsForCall)] fake.removeUnusedImportFromFileArgsForCall = append(fake.removeUnusedImportFromFileArgsForCall, struct { arg1 *token.FileSet arg2 *ast.File arg3 io.Writer - arg4 string + arg4 map[string]string }{arg1, arg2, arg3, arg4}) fake.recordInvocation("RemoveUnusedImportFromFile", []interface{}{arg1, arg2, arg3, arg4}) fake.removeUnusedImportFromFileMutex.Unlock() @@ -142,13 +142,13 @@ func (fake *FakeImportsGroomer) RemoveUnusedImportFromFileCallCount() int { return len(fake.removeUnusedImportFromFileArgsForCall) } -func (fake *FakeImportsGroomer) RemoveUnusedImportFromFileCalls(stub func(*token.FileSet, *ast.File, io.Writer, string) error) { +func (fake *FakeImportsGroomer) RemoveUnusedImportFromFileCalls(stub func(*token.FileSet, *ast.File, io.Writer, map[string]string) error) { fake.removeUnusedImportFromFileMutex.Lock() defer fake.removeUnusedImportFromFileMutex.Unlock() fake.RemoveUnusedImportFromFileStub = stub } -func (fake *FakeImportsGroomer) RemoveUnusedImportFromFileArgsForCall(i int) (*token.FileSet, *ast.File, io.Writer, string) { +func (fake *FakeImportsGroomer) RemoveUnusedImportFromFileArgsForCall(i int) (*token.FileSet, *ast.File, io.Writer, map[string]string) { fake.removeUnusedImportFromFileMutex.RLock() defer fake.removeUnusedImportFromFileMutex.RUnlock() argsForCall := fake.removeUnusedImportFromFileArgsForCall[i] @@ -178,13 +178,13 @@ func (fake *FakeImportsGroomer) RemoveUnusedImportFromFileReturnsOnCall(i int, r }{result1} } -func (fake *FakeImportsGroomer) RemoveUnusedImportFromPackage(arg1 *token.FileSet, arg2 *ast.Package, arg3 string) error { +func (fake *FakeImportsGroomer) RemoveUnusedImportFromPackage(arg1 *token.FileSet, arg2 *ast.Package, arg3 map[string]string) error { fake.removeUnusedImportFromPackageMutex.Lock() ret, specificReturn := fake.removeUnusedImportFromPackageReturnsOnCall[len(fake.removeUnusedImportFromPackageArgsForCall)] fake.removeUnusedImportFromPackageArgsForCall = append(fake.removeUnusedImportFromPackageArgsForCall, struct { arg1 *token.FileSet arg2 *ast.Package - arg3 string + arg3 map[string]string }{arg1, arg2, arg3}) fake.recordInvocation("RemoveUnusedImportFromPackage", []interface{}{arg1, arg2, arg3}) fake.removeUnusedImportFromPackageMutex.Unlock() @@ -204,13 +204,13 @@ func (fake *FakeImportsGroomer) RemoveUnusedImportFromPackageCallCount() int { return len(fake.removeUnusedImportFromPackageArgsForCall) } -func (fake *FakeImportsGroomer) RemoveUnusedImportFromPackageCalls(stub func(*token.FileSet, *ast.Package, string) error) { +func (fake *FakeImportsGroomer) RemoveUnusedImportFromPackageCalls(stub func(*token.FileSet, *ast.Package, map[string]string) error) { fake.removeUnusedImportFromPackageMutex.Lock() defer fake.removeUnusedImportFromPackageMutex.Unlock() fake.RemoveUnusedImportFromPackageStub = stub } -func (fake *FakeImportsGroomer) RemoveUnusedImportFromPackageArgsForCall(i int) (*token.FileSet, *ast.Package, string) { +func (fake *FakeImportsGroomer) RemoveUnusedImportFromPackageArgsForCall(i int) (*token.FileSet, *ast.Package, map[string]string) { fake.removeUnusedImportFromPackageMutex.RLock() defer fake.removeUnusedImportFromPackageMutex.RUnlock() argsForCall := fake.removeUnusedImportFromPackageArgsForCall[i] diff --git a/tracing/unused_imports.go b/tracing/unused_imports.go index 62b2bf7..9a5ef31 100644 --- a/tracing/unused_imports.go +++ b/tracing/unused_imports.go @@ -18,7 +18,7 @@ func NewImportsGroomer() ImportsGroomer { return &importsGroomer{} } -func (ig *importsGroomer) RemoveUnusedImportFromDirectory(path string, importToRemove string) error { +func (ig *importsGroomer) RemoveUnusedImportFromDirectory(path string, importsToRemove map[string]string) error { fset := token.NewFileSet() filter := func(info os.FileInfo) bool { return testsFilter(info) && generatedFilter(path, info) @@ -29,29 +29,31 @@ func (ig *importsGroomer) RemoveUnusedImportFromDirectory(path string, importToR } for _, pkg := range pkgs { - if err := ig.RemoveUnusedImportFromPackage(fset, pkg, importToRemove); err != nil { + if err := ig.RemoveUnusedImportFromPackage(fset, pkg, importsToRemove); err != nil { return err } } return nil } -func (ig *importsGroomer) RemoveUnusedImportFromPackage(fset *token.FileSet, pkg *ast.Package, importToRemove string) error { +func (ig *importsGroomer) RemoveUnusedImportFromPackage(fset *token.FileSet, pkg *ast.Package, importsToRemove map[string]string) error { for fileName, file := range pkg.Files { sourceFile, err := os.OpenFile(fileName, os.O_TRUNC|os.O_WRONLY, 0664) if err != nil { return fmt.Errorf("failed opening file %s: %v", fileName, err) } - if err := ig.RemoveUnusedImportFromFile(fset, file, sourceFile, importToRemove); err != nil { - return fmt.Errorf("failed removing import %s from file %s: %v", importToRemove, fileName, err) + if err := ig.RemoveUnusedImportFromFile(fset, file, sourceFile, importsToRemove); err != nil { + return fmt.Errorf("failed removing imports %v from file %s: %v", importsToRemove, fileName, err) } } return nil } -func (ig *importsGroomer) RemoveUnusedImportFromFile(fset *token.FileSet, file *ast.File, out io.Writer, importToRemove string) error { - if !astutil.UsesImport(file, importToRemove) { - astutil.DeleteImport(fset, file, importToRemove) +func (ig *importsGroomer) RemoveUnusedImportFromFile(fset *token.FileSet, file *ast.File, out io.Writer, importsToRemove map[string]string) error { + for importToRemove, alias := range importsToRemove { + if !astutil.UsesImport(file, importToRemove) { + astutil.DeleteNamedImport(fset, file, alias, importToRemove) + } } // Needed because ast does not support floating comments and deletes them. // In order to preserve all comments we just pre-parse it to dst which treats them as first class citizens. diff --git a/tracing/unused_imports_test.go b/tracing/unused_imports_test.go index ea3818b..9df91e6 100644 --- a/tracing/unused_imports_test.go +++ b/tracing/unused_imports_test.go @@ -27,7 +27,7 @@ func TestRemoveUnusedImportsFromFile(t *testing.T) { t.Fatal(err) } var buff bytes.Buffer - if err := NewImportsGroomer().RemoveUnusedImportFromFile(fset, file, &buff, "fmt"); err != nil { + if err := NewImportsGroomer().RemoveUnusedImportFromFile(fset, file, &buff, map[string]string{"fmt": "", "runtime": "rt", "crypto/rand": ""}); err != nil { t.Fatal(err) } @@ -63,7 +63,7 @@ func TestRemoveUnusedImportsFromDirectory(t *testing.T) { i++ } - if err := NewImportsGroomer().RemoveUnusedImportFromDirectory("test", "fmt"); err != nil { + if err := NewImportsGroomer().RemoveUnusedImportFromDirectory("test", map[string]string{"fmt": "", "runtime": "rt", "crypto/rand": ""}); err != nil { t.Fatal(err) } diff --git a/tracing/util.go b/tracing/util.go index 8bb2589..b72d4c3 100644 --- a/tracing/util.go +++ b/tracing/util.go @@ -39,16 +39,6 @@ func generatedFilter(path string, info os.FileInfo) bool { return true } -// Returns dst expresion like: fmt.Printf("msg\n") -func newPrintExprWithMessage(msg string) *dst.CallExpr { - return newPrintExprWithArgs([]dst.Expr{ - &dst.BasicLit{ - Kind: token.STRING, - Value: `"` + msg + `\n"`, - }, - }) -} - // Return dst expresion like: fmt.Printf(args...) func newPrintExprWithArgs(args []dst.Expr) *dst.CallExpr { return &dst.CallExpr{ @@ -59,3 +49,326 @@ func newPrintExprWithArgs(args []dst.Expr) *dst.CallExpr { Args: args, } } + +func buildEnteringFunctionArgs(f *dst.FuncDecl) []dst.Expr { + var enteringStringFormat = "Entering function %s called by %s" + args := []dst.Expr{ + &dst.BasicLit{ + Kind: token.STRING, + Value: funcNameVarName, + }, + &dst.BasicLit{ + Kind: token.STRING, + Value: callerFuncNameVarName, + }, + } + + if len(f.Type.Params.List) > 0 { + enteringStringFormat += " with args" + + for _, param := range f.Type.Params.List { + enteringStringFormat += " (%v)" + args = append(args, &dst.BasicLit{ + Kind: token.STRING, + Value: param.Names[0].Name, + }) + } + } + args = append(args, &dst.BasicLit{ + Kind: token.STRING, + Value: callIDVarName, + }) + args = append([]dst.Expr{ + &dst.BasicLit{ + Kind: token.STRING, + Value: `"` + enteringStringFormat + `; callID=%s\n"`, + }, + }, args...) + + return args +} + +func buildExitFunctionArgs() []dst.Expr { + var exitingStringFormat = "Exiting function %s called by %s; callID=%s" + return []dst.Expr{ + &dst.BasicLit{ + Kind: token.STRING, + Value: `"` + exitingStringFormat + `\n"`, + }, + &dst.BasicLit{ + Kind: token.STRING, + Value: funcNameVarName, + }, + &dst.BasicLit{ + Kind: token.STRING, + Value: callerFuncNameVarName, + }, + &dst.BasicLit{ + Kind: token.STRING, + Value: callIDVarName, + }, + } +} + +// Return dst statement like: varName := "value" +func newAssignStmt(varName, value string) *dst.AssignStmt { + return &dst.AssignStmt{ + Lhs: []dst.Expr{ + &dst.Ident{ + Name: varName, + }, + }, + Tok: token.DEFINE, + Rhs: []dst.Expr{ + &dst.BasicLit{ + Kind: token.STRING, + Value: `"` + value + `"`, + }, + }, + } +} + +/* Return dst statement like: +if funcPcVarName, _, _, ok := runtime.Caller(funcIndex); ok { + funcNameVarName = runtime.FuncForPC(funcPcVarName).Name() +} +*/ +func newGetFuncNameIfStatement(funcIndex, funcPcVarName, funcNameVarName string) *dst.IfStmt { + return &dst.IfStmt{ + Init: &dst.AssignStmt{ + Lhs: []dst.Expr{ + &dst.Ident{ + Name: funcPcVarName, + }, + &dst.Ident{ + Name: "_", + }, + &dst.Ident{ + Name: "_", + }, + &dst.Ident{ + Name: "ok", + }, + }, + Tok: token.DEFINE, + Rhs: []dst.Expr{ + &dst.CallExpr{ + Fun: &dst.SelectorExpr{ + X: &dst.Ident{ + Name: "rt", + }, + Sel: &dst.Ident{ + Name: "Caller", + }, + }, + Args: []dst.Expr{ + &dst.BasicLit{ + Kind: token.INT, + Value: funcIndex, + }, + }, + }, + }, + }, + Cond: &dst.Ident{ + Name: "ok", + }, + Body: &dst.BlockStmt{ + List: []dst.Stmt{ + &dst.AssignStmt{ + Lhs: []dst.Expr{ + &dst.Ident{ + Name: funcNameVarName, + }, + }, + Tok: token.ASSIGN, + Rhs: []dst.Expr{ + &dst.CallExpr{ + Fun: &dst.SelectorExpr{ + X: &dst.CallExpr{ + Fun: &dst.SelectorExpr{ + X: &dst.Ident{ + Name: "rt", + }, + Sel: &dst.Ident{ + Name: "FuncForPC", + }, + }, + Args: []dst.Expr{ + &dst.Ident{ + Name: funcPcVarName, + }, + }, + }, + Sel: &dst.Ident{ + Name: "Name", + }, + }, + }, + }, + }, + }, + }, + } +} + +// Returns dst statement like: +// idBytes := make([]byte, 16) +func newMakeByteSliceStmt() *dst.AssignStmt { + return &dst.AssignStmt{ + Lhs: []dst.Expr{ + &dst.Ident{ + Name: "idBytes", + }, + }, + Tok: token.DEFINE, + Rhs: []dst.Expr{ + &dst.CallExpr{ + Fun: &dst.Ident{ + Name: "make", + }, + Args: []dst.Expr{ + &dst.ArrayType{ + Elt: &dst.Ident{ + Name: "byte", + }, + }, + &dst.BasicLit{ + Kind: token.INT, + Value: "16", + }, + }, + }, + }, + } +} + +// Returns dst statement like: +// _, _ = rand.Read(idBytes) +func newRandReadStmt() *dst.AssignStmt { + return &dst.AssignStmt{ + Lhs: []dst.Expr{ + &dst.Ident{ + Name: "_", + }, + &dst.Ident{ + Name: "_", + }, + }, + Tok: token.ASSIGN, + Rhs: []dst.Expr{ + &dst.CallExpr{ + Fun: &dst.SelectorExpr{ + X: &dst.Ident{ + Name: "rand", + }, + Sel: &dst.Ident{ + Name: "Read", + }, + }, + Args: []dst.Expr{ + &dst.Ident{ + Name: "idBytes", + }, + }, + }, + }, + } +} + +// Returns dst statement like: +// callID := fmt.Sprintf("%x-%x-%x-%x-%x", idBytes[0:4], idBytes[4:6], idBytes[6:8], idBytes[8:10], idBytes[10:]) +func newParseUUIDFromByteSliceStmt(callIDVarName string) *dst.AssignStmt { + return &dst.AssignStmt{ + Lhs: []dst.Expr{ + &dst.Ident{ + Name: callIDVarName, + }, + }, + Tok: token.DEFINE, + Rhs: []dst.Expr{ + &dst.CallExpr{ + Fun: &dst.SelectorExpr{ + X: &dst.Ident{ + Name: "fmt", + }, + Sel: &dst.Ident{ + Name: "Sprintf", + }, + }, + Args: []dst.Expr{ + &dst.BasicLit{ + Kind: token.STRING, + Value: "\"%x-%x-%x-%x-%x\"", + }, + &dst.SliceExpr{ + X: &dst.Ident{ + Name: "idBytes", + }, + Low: &dst.BasicLit{ + Kind: token.INT, + Value: "0", + }, + High: &dst.BasicLit{ + Kind: token.INT, + Value: "4", + }, + Slice3: false, + }, + &dst.SliceExpr{ + X: &dst.Ident{ + Name: "idBytes", + }, + Low: &dst.BasicLit{ + Kind: token.INT, + Value: "4", + }, + High: &dst.BasicLit{ + Kind: token.INT, + Value: "6", + }, + Slice3: false, + }, + &dst.SliceExpr{ + X: &dst.Ident{ + Name: "idBytes", + }, + Low: &dst.BasicLit{ + Kind: token.INT, + Value: "6", + }, + High: &dst.BasicLit{ + Kind: token.INT, + Value: "8", + }, + Slice3: false, + }, + &dst.SliceExpr{ + X: &dst.Ident{ + Name: "idBytes", + }, + Low: &dst.BasicLit{ + Kind: token.INT, + Value: "8", + }, + High: &dst.BasicLit{ + Kind: token.INT, + Value: "10", + }, + Slice3: false, + }, + &dst.SliceExpr{ + X: &dst.Ident{ + Name: "idBytes", + }, + Low: &dst.BasicLit{ + Kind: token.INT, + Value: "10", + }, + Slice3: false, + }, + }, + }, + }, + } +} diff --git a/vis/vis.go b/vis/vis.go index 3714063..fdd9fc4 100644 --- a/vis/vis.go +++ b/vis/vis.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/DimitarPetrov/printracer/parser" "html/template" + "math" "os" ) @@ -43,15 +44,19 @@ const reportTemplate = ` # Arguments + Call ID - {{ range $i, $e := .Args }} + {{ range $i, $e := .TableRows }} {{ inc $i }} -
{{ $e }}
+
{{ $e.Args }}
+ +
{{ $e.CallID }}
+ {{ end }} @@ -138,10 +143,15 @@ func (s *stack) Empty() bool { return s.Length() == 0 } +type TableRow struct { + Args string + CallID string +} + type templateData struct { - Args []string - Diagram string - MetaJSON template.JS + TableRows []TableRow + Diagram string + MetaJSON template.JS } type sequenceDiagramData struct { @@ -149,17 +159,17 @@ type sequenceDiagramData struct { count int } -func (r *sequenceDiagramData) addFunctionInvocation(source string, target string) { +func (r *sequenceDiagramData) addFunctionInvocation(source, target string) { r.addRecord(source, "->", target) } -func (r *sequenceDiagramData) addFunctionReturn(source string, target string) { +func (r *sequenceDiagramData) addFunctionReturn(source, target string) { r.addRecord(source, "-->", target) } -func (r *sequenceDiagramData) addRecord(source string, operation string, target string) { +func (r *sequenceDiagramData) addRecord(source, operation, target string) { r.count++ - r.data.WriteString(fmt.Sprintf("%s%s%s: (%d)\n", source, operation, target, r.count)) + r.data.WriteString(fmt.Sprintf("\"%s\"%s\"%s\": (%d)\n", source, operation, target, r.count)) } func (r *sequenceDiagramData) String() string { @@ -167,18 +177,60 @@ func (r *sequenceDiagramData) String() string { } func (v *visualizer) constructTemplateData(events []parser.FuncEvent, maxDepth int, startingFunc string) (templateData, error) { + if maxDepth == math.MaxInt32 && len(startingFunc) == 0 { + return v.constructTemplateDataGraph(events) + } + return v.constructTemplateDataLinearly(events, maxDepth, startingFunc) +} + +func (v *visualizer) constructTemplateDataGraph(events []parser.FuncEvent) (templateData, error) { + diagramData := &sequenceDiagramData{} + + var tableRows []TableRow + + for i := 0; i < len(events); i++ { + event := events[i] + switch event := event.(type) { + case *parser.InvocationEvent: + diagramData.addFunctionInvocation(event.GetCaller(), event.GetCallee()) + tableRows = append(tableRows, TableRow{ + Args: fmt.Sprintf("calling %s", event.Args), + CallID: event.GetCallID(), + }) + case *parser.ReturningEvent: + diagramData.addFunctionReturn(event.GetCallee(), event.GetCaller()) + tableRows = append(tableRows, TableRow{ + Args: "returning", + CallID: event.GetCallID(), + }) + } + } + + return templateData{ + Diagram: diagramData.String(), + TableRows: tableRows, + }, nil +} + +func (v *visualizer) constructTemplateDataLinearly(events []parser.FuncEvent, maxDepth int, startingFunc string) (templateData, error) { diagramData := &sequenceDiagramData{} if len(startingFunc) > 0 { + found := false for i := 0; i < len(events); i++ { - if events[i].FuncName() == startingFunc { + if _, ok := events[i].(*parser.InvocationEvent); ok && events[i].GetCaller() == startingFunc { events = events[i:] + found = true break } } + if !found { + return templateData{}, fmt.Errorf("could not find functions called by %s", startingFunc) + } + for i := 1; i < len(events); i++ { - if events[i].FuncName() == startingFunc { + if _, ok := events[i].(*parser.ReturningEvent); ok && events[i].GetCaller() == startingFunc { events = events[:i+1] break } @@ -186,36 +238,47 @@ func (v *visualizer) constructTemplateData(events []parser.FuncEvent, maxDepth i } stack := stack(make([]parser.FuncEvent, 0, len(events))) + var tableRows []TableRow - var args []string + diagramData.addFunctionInvocation(events[0].GetCaller(), events[0].GetCallee()) + stack.Push(events[0]) + tableRows = append(tableRows, TableRow{ + Args: fmt.Sprintf("calling %s", events[0].(*parser.InvocationEvent).Args), + CallID: events[0].GetCallID(), + }) - for i := 0; i < len(events); i++ { + for i := 1; i < len(events); i++ { + if stack.Empty() { + break + } event := events[i] switch event := event.(type) { case *parser.InvocationEvent: if stack.Length() < maxDepth { - prev := event - if !stack.Empty() { - prev = stack.Peek().(*parser.InvocationEvent) + prev := stack.Peek().(*parser.InvocationEvent) + if prev.GetCallee() == event.GetCaller() { + diagramData.addFunctionInvocation(event.GetCaller(), event.GetCallee()) + tableRows = append(tableRows, TableRow{ + Args: fmt.Sprintf("calling %s", event.Args), + CallID: event.GetCallID(), + }) + stack.Push(event) } - diagramData.addFunctionInvocation(prev.Name, event.FuncName()) - args = append(args, fmt.Sprintf("calling %s", event.Args)) - stack.Push(event) } case *parser.ReturningEvent: - if stack.Peek().FuncName() == event.FuncName() { - prev := stack.Pop() - if !stack.Empty() { - prev = stack.Peek() - } - diagramData.addFunctionReturn(event.FuncName(), prev.FuncName()) - args = append(args, "returning") + if stack.Peek().GetCallee() == event.GetCallee() { + _ = stack.Pop() + diagramData.addFunctionReturn(event.GetCallee(), event.GetCaller()) + tableRows = append(tableRows, TableRow{ + Args: "returning", + CallID: event.GetCallID(), + }) } } } return templateData{ - Diagram: diagramData.String(), - Args: args, + Diagram: diagramData.String(), + TableRows: tableRows, }, nil } diff --git a/vis/vis_test.go b/vis/vis_test.go index 8372321..c1f45ce 100644 --- a/vis/vis_test.go +++ b/vis/vis_test.go @@ -13,83 +13,99 @@ import ( var inputEvents = []parser.FuncEvent{ &parser.InvocationEvent{ - Name: "main", + Caller: "runtime.main", + Callee: "main.main", + CallID: "1d8ca74e-c860-8a75-fc36-fe6d34350f0c", }, &parser.InvocationEvent{ - Name: "foo", - Args: "with args (1) (true)", + Caller: "main.main", + Callee: "main.foo", + CallID: "973355a9-2ec6-095c-9137-7a1081ac0a5f", + Args: "with args (5) (false)", }, &parser.InvocationEvent{ - Name: "bar", - Args: "with args (test string)", + Caller: "main.foo", + Callee: "main.bar", + CallID: "6c294dfd-4c6a-39b1-474e-314bee73f514", + Args: "with args (test string)", }, &parser.InvocationEvent{ - Name: "baz", + Caller: "main.bar", + Callee: "main.baz", + CallID: "a019a297-0a6e-a792-0e3f-23c33a44622f", + }, + &parser.ReturningEvent{ + Caller: "main.bar", + Callee: "main.baz", + CallID: "a019a297-0a6e-a792-0e3f-23c33a44622f", + }, + &parser.ReturningEvent{ + Caller: "main.foo", + Callee: "main.bar", + CallID: "6c294dfd-4c6a-39b1-474e-314bee73f514", + }, + &parser.ReturningEvent{ + Caller: "main.main", + Callee: "main.foo", + CallID: "973355a9-2ec6-095c-9137-7a1081ac0a5f", + }, + &parser.ReturningEvent{ + Caller: "runtime.main", + Callee: "main.main", + CallID: "1d8ca74e-c860-8a75-fc36-fe6d34350f0c", }, - &parser.ReturningEvent{Name: "baz"}, - &parser.ReturningEvent{Name: "bar"}, - &parser.ReturningEvent{Name: "foo"}, - &parser.ReturningEvent{Name: "main"}, } -var fullDiagram = `main->main: (1) -main->foo: (2) -foo->bar: (3) -bar->baz: (4) -baz-->bar: (5) -bar-->foo: (6) -foo-->main: (7) -main-->main: (8) +var fullDiagram = `"runtime.main"->"main.main": (1) +"main.main"->"main.foo": (2) +"main.foo"->"main.bar": (3) +"main.bar"->"main.baz": (4) +"main.baz"-->"main.bar": (5) +"main.bar"-->"main.foo": (6) +"main.foo"-->"main.main": (7) +"main.main"-->"runtime.main": (8) ` -var fullArgs = []string{ - "calling ", - "calling with args (1) (true)", - "calling with args (test string)", - "calling ", - "returning", - "returning", - "returning", - "returning", +var fullTableRows = []TableRow{ + {Args: "calling ", CallID: "1d8ca74e-c860-8a75-fc36-fe6d34350f0c"}, + {Args: "calling with args (5) (false)", CallID: "973355a9-2ec6-095c-9137-7a1081ac0a5f"}, + {Args: "calling with args (test string)", CallID: "6c294dfd-4c6a-39b1-474e-314bee73f514"}, + {Args: "calling ", CallID: "a019a297-0a6e-a792-0e3f-23c33a44622f"}, + {Args: "returning", CallID: "a019a297-0a6e-a792-0e3f-23c33a44622f"}, + {Args: "returning", CallID: "6c294dfd-4c6a-39b1-474e-314bee73f514"}, + {Args: "returning", CallID: "973355a9-2ec6-095c-9137-7a1081ac0a5f"}, + {Args: "returning", CallID: "1d8ca74e-c860-8a75-fc36-fe6d34350f0c"}, } -var diagramWith2DepthLimit = `main->main: (1) -main->foo: (2) -foo-->main: (3) -main-->main: (4) +var diagramWith2DepthLimit = `"runtime.main"->"main.main": (1) +"main.main"->"main.foo": (2) +"main.foo"-->"main.main": (3) +"main.main"-->"runtime.main": (4) ` -var argsWith2DepthLimit = []string{ - "calling ", - "calling with args (1) (true)", - "returning", - "returning", +var tableRowsWith2DepthLimit = []TableRow{ + {Args: "calling ", CallID: "1d8ca74e-c860-8a75-fc36-fe6d34350f0c"}, + {Args: "calling with args (5) (false)", CallID: "973355a9-2ec6-095c-9137-7a1081ac0a5f"}, + {Args: "returning", CallID: "973355a9-2ec6-095c-9137-7a1081ac0a5f"}, + {Args: "returning", CallID: "1d8ca74e-c860-8a75-fc36-fe6d34350f0c"}, } -var diagramWithFooStartingFunc = `foo->foo: (1) -foo->bar: (2) -bar->baz: (3) -baz-->bar: (4) -bar-->foo: (5) -foo-->foo: (6) +var diagramWithFooStartingFunc = `"main.foo"->"main.bar": (1) +"main.bar"->"main.baz": (2) +"main.baz"-->"main.bar": (3) +"main.bar"-->"main.foo": (4) ` -var argsWithFooStartingFunc = []string{ - "calling with args (1) (true)", - "calling with args (test string)", - "calling ", - "returning", - "returning", - "returning", +var tableRowsWithFooStartingFunc = []TableRow{ + {Args: "calling with args (test string)", CallID: "6c294dfd-4c6a-39b1-474e-314bee73f514"}, + {Args: "calling ", CallID: "a019a297-0a6e-a792-0e3f-23c33a44622f"}, + {Args: "returning", CallID: "a019a297-0a6e-a792-0e3f-23c33a44622f"}, + {Args: "returning", CallID: "6c294dfd-4c6a-39b1-474e-314bee73f514"}, } -var diagramWithFooStartingFuncAnd2DepthLimit = `foo->foo: (1) -foo->bar: (2) -bar-->foo: (3) -foo-->foo: (4) +var diagramWithFooStartingFuncAnd2DepthLimit = `"main.foo"->"main.bar": (1) +"main.bar"-->"main.foo": (2) ` -var argsWithFooStartingFuncAnd2DepthLimit = []string{ - "calling with args (1) (true)", - "calling with args (test string)", - "returning", - "returning", +var tableRowsWithFooStartingFuncAnd2DepthLimit = []TableRow{ + {Args: "calling with args (test string)", CallID: "6c294dfd-4c6a-39b1-474e-314bee73f514"}, + {Args: "returning", CallID: "6c294dfd-4c6a-39b1-474e-314bee73f514"}, } func TestVisualizerConstructTemplateData(t *testing.T) { @@ -98,12 +114,12 @@ func TestVisualizerConstructTemplateData(t *testing.T) { MaxDepth int StartingFunc string Diagram string - Args []string + TableRows []TableRow }{ - {Name: "ConstructTemplateData", MaxDepth: math.MaxInt32, Diagram: fullDiagram, Args: fullArgs}, - {Name: "ConstructTemplateDataWithDepthLimit", MaxDepth: 2, Diagram: diagramWith2DepthLimit, Args: argsWith2DepthLimit}, - {Name: "ConstructTemplateDataWithFooStartingFunc", MaxDepth: math.MaxInt32, StartingFunc: "foo", Diagram: diagramWithFooStartingFunc, Args: argsWithFooStartingFunc}, - {Name: "ConstructTemplateDataWithFooStartingFuncAndDepthLimit", MaxDepth: 2, StartingFunc: "foo", Diagram: diagramWithFooStartingFuncAnd2DepthLimit, Args: argsWithFooStartingFuncAnd2DepthLimit}, + {Name: "ConstructTemplateData", MaxDepth: math.MaxInt32, Diagram: fullDiagram, TableRows: fullTableRows}, + {Name: "ConstructTemplateDataWithDepthLimit", MaxDepth: 2, Diagram: diagramWith2DepthLimit, TableRows: tableRowsWith2DepthLimit}, + {Name: "ConstructTemplateDataWithFooStartingFunc", MaxDepth: math.MaxInt32, StartingFunc: "main.foo", Diagram: diagramWithFooStartingFunc, TableRows: tableRowsWithFooStartingFunc}, + {Name: "ConstructTemplateDataWithFooStartingFuncAndDepthLimit", MaxDepth: 1, StartingFunc: "main.foo", Diagram: diagramWithFooStartingFuncAnd2DepthLimit, TableRows: tableRowsWithFooStartingFuncAnd2DepthLimit}, } visualizer := visualizer{} @@ -117,8 +133,8 @@ func TestVisualizerConstructTemplateData(t *testing.T) { if diagramData.Diagram != test.Diagram { t.Errorf("Assertion failed! Expected diagram data: %s bug got: %s", test.Diagram, diagramData.Diagram) } - if !reflect.DeepEqual(diagramData.Args, test.Args) { - t.Errorf("Assertion failed! Expected args: %v bug got: %v", test.Args, diagramData.Args) + if !reflect.DeepEqual(diagramData.TableRows, test.TableRows) { + t.Errorf("Assertion failed! Expected args: %v bug got: %v", test.TableRows, diagramData.TableRows) } }) } @@ -130,12 +146,12 @@ func TestVisualize(t *testing.T) { MaxDepth int StartingFunc string Diagram string - Args []string + TableRows []TableRow }{ - {Name: "ConstructTemplateData", MaxDepth: math.MaxInt32, Diagram: fullDiagram, Args: fullArgs}, - {Name: "ConstructTemplateDataWithDepthLimit", MaxDepth: 2, Diagram: diagramWith2DepthLimit, Args: argsWith2DepthLimit}, - {Name: "ConstructTemplateDataWithFooStartingFunc", MaxDepth: math.MaxInt32, StartingFunc: "foo", Diagram: diagramWithFooStartingFunc, Args: argsWithFooStartingFunc}, - {Name: "ConstructTemplateDataWithFooStartingFuncAndDepthLimit", MaxDepth: 2, StartingFunc: "foo", Diagram: diagramWithFooStartingFuncAnd2DepthLimit, Args: argsWithFooStartingFuncAnd2DepthLimit}, + {Name: "ConstructTemplateData", MaxDepth: math.MaxInt32, Diagram: fullDiagram, TableRows: fullTableRows}, + {Name: "ConstructTemplateDataWithDepthLimit", MaxDepth: 2, Diagram: diagramWith2DepthLimit, TableRows: tableRowsWith2DepthLimit}, + {Name: "ConstructTemplateDataWithFooStartingFunc", MaxDepth: math.MaxInt32, StartingFunc: "main.foo", Diagram: diagramWithFooStartingFunc, TableRows: tableRowsWithFooStartingFunc}, + {Name: "ConstructTemplateDataWithFooStartingFuncAndDepthLimit", MaxDepth: 1, StartingFunc: "main.foo", Diagram: diagramWithFooStartingFuncAnd2DepthLimit, TableRows: tableRowsWithFooStartingFuncAnd2DepthLimit}, } for _, test := range tests { @@ -155,13 +171,16 @@ func TestVisualize(t *testing.T) { t.Fatal(err) } - encodedDiagram := strings.ReplaceAll(strings.ReplaceAll(test.Diagram, "->", `-\x3e`), "\n", `\n`) + encodedDiagram := strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(test.Diagram, "->", `-\x3e`), "\n", `\n`), `"`, `\x22`) if !bytes.Contains(html, []byte(encodedDiagram)) { t.Error("Assertion failed! Expected html file to contain diagram data") } - for _, arg := range test.Args { - if !bytes.Contains(html, []byte(arg)) { - t.Errorf("Assertion failed! Expected html file to contain arg %s", arg) + for _, row := range test.TableRows { + if !bytes.Contains(html, []byte(row.Args)) { + t.Errorf("Assertion failed! Expected html file to contain arg %s", row.Args) + } + if !bytes.Contains(html, []byte(row.CallID)) { + t.Errorf("Assertion failed! Expected html file to contain callID %s", row.CallID) } } })