diff options
| author | Dmitry Vyukov <dvyukov@google.com> | 2017-06-13 19:31:19 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2017-06-13 19:31:19 +0200 |
| commit | 5b060131006494cbc077f08b9b2fbf172f3eb239 (patch) | |
| tree | 04f8586899db96f7fd8e7bc6a010fc10f1e2bb3b /vendor/cloud.google.com/go/cmd/go-cloud-debug-agent | |
| parent | cd8e13f826ff24f5f8e0b8de1b9d3373aaf93d2f (diff) | |
| parent | 612b82714b3e6660bf702f801ab96aacb3432e1f (diff) | |
Merge pull request #226 from google/dvyukov-vendor
vendor: vendor dependencies
Diffstat (limited to 'vendor/cloud.google.com/go/cmd/go-cloud-debug-agent')
7 files changed, 2215 insertions, 0 deletions
diff --git a/vendor/cloud.google.com/go/cmd/go-cloud-debug-agent/debuglet.go b/vendor/cloud.google.com/go/cmd/go-cloud-debug-agent/debuglet.go new file mode 100644 index 000000000..6a8702c77 --- /dev/null +++ b/vendor/cloud.google.com/go/cmd/go-cloud-debug-agent/debuglet.go @@ -0,0 +1,450 @@ +// Copyright 2016 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +build linux + +package main + +import ( + "encoding/json" + "flag" + "fmt" + "io/ioutil" + "log" + "math/rand" + "os" + "sync" + "time" + + "cloud.google.com/go/cmd/go-cloud-debug-agent/internal/breakpoints" + debuglet "cloud.google.com/go/cmd/go-cloud-debug-agent/internal/controller" + "cloud.google.com/go/cmd/go-cloud-debug-agent/internal/valuecollector" + "cloud.google.com/go/compute/metadata" + "golang.org/x/debug" + "golang.org/x/debug/local" + "golang.org/x/net/context" + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" + cd "google.golang.org/api/clouddebugger/v2" +) + +var ( + appModule = flag.String("appmodule", "", "Optional application module name.") + appVersion = flag.String("appversion", "", "Optional application module version name.") + sourceContextFile = flag.String("sourcecontext", "", "File containing JSON-encoded source context.") + verbose = flag.Bool("v", false, "Output verbose log messages.") + projectNumber = flag.String("projectnumber", "", "Project number."+ + " If this is not set, it is read from the GCP metadata server.") + projectID = flag.String("projectid", "", "Project ID."+ + " If this is not set, it is read from the GCP metadata server.") + serviceAccountFile = flag.String("serviceaccountfile", "", "File containing JSON service account credentials.") +) + +const ( + maxCapturedStackFrames = 50 + maxCapturedVariables = 1000 +) + +func main() { + flag.Usage = usage + flag.Parse() + args := flag.Args() + if len(args) == 0 { + // The user needs to supply the name of the executable to run. + flag.Usage() + return + } + if *projectNumber == "" { + var err error + *projectNumber, err = metadata.NumericProjectID() + if err != nil { + log.Print("Debuglet initialization: ", err) + } + } + if *projectID == "" { + var err error + *projectID, err = metadata.ProjectID() + if err != nil { + log.Print("Debuglet initialization: ", err) + } + } + sourceContexts, err := readSourceContextFile(*sourceContextFile) + if err != nil { + log.Print("Reading source context file: ", err) + } + var ts oauth2.TokenSource + ctx := context.Background() + if *serviceAccountFile != "" { + if ts, err = serviceAcctTokenSource(ctx, *serviceAccountFile, cd.CloudDebuggerScope); err != nil { + log.Fatalf("Error getting credentials from file %s: %v", *serviceAccountFile, err) + } + } else if ts, err = google.DefaultTokenSource(ctx, cd.CloudDebuggerScope); err != nil { + log.Print("Error getting application default credentials for Cloud Debugger:", err) + os.Exit(103) + } + c, err := debuglet.NewController(ctx, debuglet.Options{ + ProjectNumber: *projectNumber, + ProjectID: *projectID, + AppModule: *appModule, + AppVersion: *appVersion, + SourceContexts: sourceContexts, + Verbose: *verbose, + TokenSource: ts, + }) + if err != nil { + log.Fatal("Error connecting to Cloud Debugger: ", err) + } + prog, err := local.New(args[0]) + if err != nil { + log.Fatal("Error loading program: ", err) + } + // Load the program, but don't actually start it running yet. + if _, err = prog.Run(args[1:]...); err != nil { + log.Fatal("Error loading program: ", err) + } + bs := breakpoints.NewBreakpointStore(prog) + + // Seed the random number generator. + rand.Seed(time.Now().UnixNano()) + + // Now we want to do two things: run the user's program, and start sending + // List requests periodically to the Debuglet Controller to get breakpoints + // to set. + // + // We want to give the Debuglet Controller a chance to give us breakpoints + // before we start the program, otherwise we would miss any breakpoint + // triggers that occur during program startup -- for example, a breakpoint on + // the first line of main. But if the Debuglet Controller is not responding or + // is returning errors, we don't want to delay starting the program + // indefinitely. + // + // We pass a channel to breakpointListLoop, which will close it when the first + // List call finishes. Then we wait until either the channel is closed or a + // 5-second timer has finished before starting the program. + ch := make(chan bool) + // Start a goroutine that sends List requests to the Debuglet Controller, and + // sets any breakpoints it gets back. + go breakpointListLoop(ctx, c, bs, ch) + // Wait until 5 seconds have passed or breakpointListLoop has closed ch. + select { + case <-time.After(5 * time.Second): + case <-ch: + } + // Run the debuggee. + programLoop(ctx, c, bs, prog) +} + +// usage prints a usage message to stderr and exits. +func usage() { + me := "a.out" + if len(os.Args) >= 1 { + me = os.Args[0] + } + fmt.Fprintf(os.Stderr, "Usage of %s:\n", me) + fmt.Fprintf(os.Stderr, "\t%s [flags...] -- <program name> args...\n", me) + fmt.Fprintf(os.Stderr, "Flags:\n") + flag.PrintDefaults() + fmt.Fprintf(os.Stderr, + "See https://cloud.google.com/tools/cloud-debugger/setting-up-on-compute-engine for more information.\n") + os.Exit(2) +} + +// readSourceContextFile reads a JSON-encoded source context from the given file. +// It returns a non-empty slice on success. +func readSourceContextFile(filename string) ([]*cd.SourceContext, error) { + if filename == "" { + return nil, nil + } + scJSON, err := ioutil.ReadFile(filename) + if err != nil { + return nil, fmt.Errorf("reading file %q: %v", filename, err) + } + var sc cd.SourceContext + if err = json.Unmarshal(scJSON, &sc); err != nil { + return nil, fmt.Errorf("parsing file %q: %v", filename, err) + } + return []*cd.SourceContext{&sc}, nil +} + +// breakpointListLoop repeatedly calls the Debuglet Controller's List RPC, and +// passes the results to the BreakpointStore so it can set and unset breakpoints +// in the program. +// +// After the first List call finishes, ch is closed. +func breakpointListLoop(ctx context.Context, c *debuglet.Controller, bs *breakpoints.BreakpointStore, first chan bool) { + const ( + avgTimeBetweenCalls = time.Second + errorDelay = 5 * time.Second + ) + + // randomDuration returns a random duration with expected value avg. + randomDuration := func(avg time.Duration) time.Duration { + return time.Duration(rand.Int63n(int64(2*avg + 1))) + } + + var consecutiveFailures uint + + for { + callStart := time.Now() + resp, err := c.List(ctx) + if err != nil && err != debuglet.ErrListUnchanged { + log.Printf("Debuglet controller server error: %v", err) + } + if err == nil { + bs.ProcessBreakpointList(resp.Breakpoints) + } + + if first != nil { + // We've finished one call to List and set any breakpoints we received. + close(first) + first = nil + } + + // Asynchronously send updates for any breakpoints that caused an error when + // the BreakpointStore tried to process them. We don't wait for the update + // to finish before the program can exit, as we do for normal updates. + errorBps := bs.ErrorBreakpoints() + for _, bp := range errorBps { + go func(bp *cd.Breakpoint) { + if err := c.Update(ctx, bp.Id, bp); err != nil { + log.Printf("Failed to send breakpoint update for %s: %s", bp.Id, err) + } + }(bp) + } + + // Make the next call not too soon after the one we just did. + delay := randomDuration(avgTimeBetweenCalls) + + // If the call returned an error other than ErrListUnchanged, wait longer. + if err != nil && err != debuglet.ErrListUnchanged { + // Wait twice as long after each consecutive failure, to a maximum of 16x. + delay += randomDuration(errorDelay * (1 << consecutiveFailures)) + if consecutiveFailures < 4 { + consecutiveFailures++ + } + } else { + consecutiveFailures = 0 + } + + // Sleep until we reach time callStart+delay. If we've already passed that + // time, time.Sleep will return immediately -- this should be the common + // case, since the server will delay responding to List for a while when + // there are no changes to report. + time.Sleep(callStart.Add(delay).Sub(time.Now())) + } +} + +// programLoop runs the program being debugged to completion. When a breakpoint's +// conditions are satisfied, it sends an Update RPC to the Debuglet Controller. +// The function returns when the program exits and all Update RPCs have finished. +func programLoop(ctx context.Context, c *debuglet.Controller, bs *breakpoints.BreakpointStore, prog debug.Program) { + var wg sync.WaitGroup + for { + // Run the program until it hits a breakpoint or exits. + status, err := prog.Resume() + if err != nil { + break + } + + // Get the breakpoints at this address whose conditions were satisfied, + // and remove the ones that aren't logpoints. + bps := bs.BreakpointsAtPC(status.PC) + bps = bpsWithConditionSatisfied(bps, prog) + for _, bp := range bps { + if bp.Action != "LOG" { + bs.RemoveBreakpoint(bp) + } + } + + if len(bps) == 0 { + continue + } + + // Evaluate expressions and get the stack. + vc := valuecollector.NewCollector(prog, maxCapturedVariables) + needStackFrames := false + for _, bp := range bps { + // If evaluating bp's condition didn't return an error, evaluate bp's + // expressions, and later get the stack frames. + if bp.Status == nil { + bp.EvaluatedExpressions = expressionValues(bp.Expressions, prog, vc) + needStackFrames = true + } + } + var ( + stack []*cd.StackFrame + stackFramesStatusMessage *cd.StatusMessage + ) + if needStackFrames { + stack, stackFramesStatusMessage = stackFrames(prog, vc) + } + + // Read variable values from the program. + variableTable := vc.ReadValues() + + // Start a goroutine to send updates to the Debuglet Controller or write + // to logs, concurrently with resuming the program. + // TODO: retry Update on failure. + for _, bp := range bps { + wg.Add(1) + switch bp.Action { + case "LOG": + go func(format string, evaluatedExpressions []*cd.Variable) { + s := valuecollector.LogString(format, evaluatedExpressions, variableTable) + log.Print(s) + wg.Done() + }(bp.LogMessageFormat, bp.EvaluatedExpressions) + bp.Status = nil + bp.EvaluatedExpressions = nil + default: + go func(bp *cd.Breakpoint) { + defer wg.Done() + bp.IsFinalState = true + if bp.Status == nil { + // If evaluating bp's condition didn't return an error, include the + // stack frames, variable table, and any status message produced when + // getting the stack frames. + bp.StackFrames = stack + bp.VariableTable = variableTable + bp.Status = stackFramesStatusMessage + } + if err := c.Update(ctx, bp.Id, bp); err != nil { + log.Printf("Failed to send breakpoint update for %s: %s", bp.Id, err) + } + }(bp) + } + } + } + + // Wait for all updates to finish before returning. + wg.Wait() +} + +// bpsWithConditionSatisfied returns the breakpoints whose conditions are true +// (or that do not have a condition.) +func bpsWithConditionSatisfied(bpsIn []*cd.Breakpoint, prog debug.Program) []*cd.Breakpoint { + var bpsOut []*cd.Breakpoint + for _, bp := range bpsIn { + cond, err := condTruth(bp.Condition, prog) + if err != nil { + bp.Status = errorStatusMessage(err.Error(), refersToBreakpointCondition) + // Include bp in the list to be updated when there's an error, so that + // the user gets a response. + bpsOut = append(bpsOut, bp) + } else if cond { + bpsOut = append(bpsOut, bp) + } + } + return bpsOut +} + +// condTruth evaluates a condition. +func condTruth(condition string, prog debug.Program) (bool, error) { + if condition == "" { + // A condition wasn't set. + return true, nil + } + val, err := prog.Evaluate(condition) + if err != nil { + return false, err + } + if v, ok := val.(bool); !ok { + return false, fmt.Errorf("condition expression has type %T, should be bool", val) + } else { + return v, nil + } +} + +// expressionValues evaluates a slice of expressions and returns a []*cd.Variable +// containing the results. +// If the result of an expression evaluation refers to values from the program's +// memory (e.g., the expression evaluates to a slice) a corresponding variable is +// added to the value collector, to be read later. +func expressionValues(expressions []string, prog debug.Program, vc *valuecollector.Collector) []*cd.Variable { + evaluatedExpressions := make([]*cd.Variable, len(expressions)) + for i, exp := range expressions { + ee := &cd.Variable{Name: exp} + evaluatedExpressions[i] = ee + if val, err := prog.Evaluate(exp); err != nil { + ee.Status = errorStatusMessage(err.Error(), refersToBreakpointExpression) + } else { + vc.FillValue(val, ee) + } + } + return evaluatedExpressions +} + +// stackFrames returns a stack trace for the program. It passes references to +// function parameters and local variables to the value collector, so it can read +// their values later. +func stackFrames(prog debug.Program, vc *valuecollector.Collector) ([]*cd.StackFrame, *cd.StatusMessage) { + frames, err := prog.Frames(maxCapturedStackFrames) + if err != nil { + return nil, errorStatusMessage("Error getting stack: "+err.Error(), refersToUnspecified) + } + stackFrames := make([]*cd.StackFrame, len(frames)) + for i, f := range frames { + frame := &cd.StackFrame{} + frame.Function = f.Function + for _, v := range f.Params { + frame.Arguments = append(frame.Arguments, vc.AddVariable(debug.LocalVar(v))) + } + for _, v := range f.Vars { + frame.Locals = append(frame.Locals, vc.AddVariable(v)) + } + frame.Location = &cd.SourceLocation{ + Path: f.File, + Line: int64(f.Line), + } + stackFrames[i] = frame + } + return stackFrames, nil +} + +// errorStatusMessage returns a *cd.StatusMessage indicating an error, +// with the given message and refersTo field. +func errorStatusMessage(msg string, refersTo int) *cd.StatusMessage { + return &cd.StatusMessage{ + Description: &cd.FormatMessage{Format: "$0", Parameters: []string{msg}}, + IsError: true, + RefersTo: refersToString[refersTo], + } +} + +const ( + // RefersTo values for cd.StatusMessage. + refersToUnspecified = iota + refersToBreakpointCondition + refersToBreakpointExpression +) + +// refersToString contains the strings for each refersTo value. +// See the definition of StatusMessage in the v2/clouddebugger package. +var refersToString = map[int]string{ + refersToUnspecified: "UNSPECIFIED", + refersToBreakpointCondition: "BREAKPOINT_CONDITION", + refersToBreakpointExpression: "BREAKPOINT_EXPRESSION", +} + +func serviceAcctTokenSource(ctx context.Context, filename string, scope ...string) (oauth2.TokenSource, error) { + data, err := ioutil.ReadFile(filename) + if err != nil { + return nil, fmt.Errorf("cannot read service account file: %v", err) + } + cfg, err := google.JWTConfigFromJSON(data, scope...) + if err != nil { + return nil, fmt.Errorf("google.JWTConfigFromJSON: %v", err) + } + return cfg.TokenSource(ctx), nil +} diff --git a/vendor/cloud.google.com/go/cmd/go-cloud-debug-agent/internal/breakpoints/breakpoints.go b/vendor/cloud.google.com/go/cmd/go-cloud-debug-agent/internal/breakpoints/breakpoints.go new file mode 100644 index 000000000..afe07cbfb --- /dev/null +++ b/vendor/cloud.google.com/go/cmd/go-cloud-debug-agent/internal/breakpoints/breakpoints.go @@ -0,0 +1,174 @@ +// Copyright 2016 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package breakpoints handles breakpoint requests we get from the user through +// the Debuglet Controller, and manages corresponding breakpoints set in the code. +package breakpoints + +import ( + "log" + "sync" + + "golang.org/x/debug" + cd "google.golang.org/api/clouddebugger/v2" +) + +// BreakpointStore stores the set of breakpoints for a program. +type BreakpointStore struct { + mu sync.Mutex + // prog is the program being debugged. + prog debug.Program + // idToBreakpoint is a map from breakpoint identifier to *cd.Breakpoint. The + // map value is nil if the breakpoint is inactive. A breakpoint is active if: + // - We received it from the Debuglet Controller, and it was active at the time; + // - We were able to set code breakpoints for it; + // - We have not reached any of those code breakpoints while satisfying the + // breakpoint's conditions, or the breakpoint has action LOG; and + // - The Debuglet Controller hasn't informed us the breakpoint has become inactive. + idToBreakpoint map[string]*cd.Breakpoint + // pcToBps and bpToPCs store the many-to-many relationship between breakpoints we + // received from the Debuglet Controller and the code breakpoints we set for them. + pcToBps map[uint64][]*cd.Breakpoint + bpToPCs map[*cd.Breakpoint][]uint64 + // errors contains any breakpoints which couldn't be set because they caused an + // error. These are retrieved with ErrorBreakpoints, and the caller is + // expected to handle sending updates for them. + errors []*cd.Breakpoint +} + +// NewBreakpointStore returns a BreakpointStore for the given program. +func NewBreakpointStore(prog debug.Program) *BreakpointStore { + return &BreakpointStore{ + idToBreakpoint: make(map[string]*cd.Breakpoint), + pcToBps: make(map[uint64][]*cd.Breakpoint), + bpToPCs: make(map[*cd.Breakpoint][]uint64), + prog: prog, + } +} + +// ProcessBreakpointList applies updates received from the Debuglet Controller through a List call. +func (bs *BreakpointStore) ProcessBreakpointList(bps []*cd.Breakpoint) { + bs.mu.Lock() + defer bs.mu.Unlock() + for _, bp := range bps { + if storedBp, ok := bs.idToBreakpoint[bp.Id]; ok { + if storedBp != nil && bp.IsFinalState { + // IsFinalState indicates that the breakpoint has been made inactive. + bs.removeBreakpointLocked(storedBp) + } + } else { + if bp.IsFinalState { + // The controller is notifying us that the breakpoint is no longer active, + // but we didn't know about it anyway. + continue + } + if bp.Action != "" && bp.Action != "CAPTURE" && bp.Action != "LOG" { + bp.IsFinalState = true + bp.Status = &cd.StatusMessage{ + Description: &cd.FormatMessage{Format: "Action is not supported"}, + IsError: true, + } + bs.errors = append(bs.errors, bp) + // Note in idToBreakpoint that we've already seen this breakpoint, so that we + // don't try to report it as an error multiple times. + bs.idToBreakpoint[bp.Id] = nil + continue + } + pcs, err := bs.prog.BreakpointAtLine(bp.Location.Path, uint64(bp.Location.Line)) + if err != nil { + log.Printf("error setting breakpoint at %s:%d: %v", bp.Location.Path, bp.Location.Line, err) + } + if len(pcs) == 0 { + // We can't find a PC for this breakpoint's source line, so don't make it active. + // TODO: we could snap the line to a location where we can break, or report an error to the user. + bs.idToBreakpoint[bp.Id] = nil + } else { + bs.idToBreakpoint[bp.Id] = bp + for _, pc := range pcs { + bs.pcToBps[pc] = append(bs.pcToBps[pc], bp) + } + bs.bpToPCs[bp] = pcs + } + } + } +} + +// ErrorBreakpoints returns a slice of Breakpoints that caused errors when the +// BreakpointStore tried to process them, and resets the list of such +// breakpoints. +// The caller is expected to send updates to the server to indicate the errors. +func (bs *BreakpointStore) ErrorBreakpoints() []*cd.Breakpoint { + bs.mu.Lock() + defer bs.mu.Unlock() + bps := bs.errors + bs.errors = nil + return bps +} + +// BreakpointsAtPC returns all the breakpoints for which we set a code +// breakpoint at the given address. +func (bs *BreakpointStore) BreakpointsAtPC(pc uint64) []*cd.Breakpoint { + bs.mu.Lock() + defer bs.mu.Unlock() + return bs.pcToBps[pc] +} + +// RemoveBreakpoint makes the given breakpoint inactive. +// This is called when either the debugged program hits the breakpoint, or the Debuglet +// Controller informs us that the breakpoint is now inactive. +func (bs *BreakpointStore) RemoveBreakpoint(bp *cd.Breakpoint) { + bs.mu.Lock() + bs.removeBreakpointLocked(bp) + bs.mu.Unlock() +} + +func (bs *BreakpointStore) removeBreakpointLocked(bp *cd.Breakpoint) { + // Set the ID's corresponding breakpoint to nil, so that we won't activate it + // if we see it again. + // TODO: we could delete it after a few seconds. + bs.idToBreakpoint[bp.Id] = nil + + // Delete bp from the list of cd breakpoints at each of its corresponding + // code breakpoint locations, and delete any code breakpoints which no longer + // have a corresponding cd breakpoint. + var codeBreakpointsToDelete []uint64 + for _, pc := range bs.bpToPCs[bp] { + bps := remove(bs.pcToBps[pc], bp) + if len(bps) == 0 { + // bp was the last breakpoint set at this PC, so delete the code breakpoint. + codeBreakpointsToDelete = append(codeBreakpointsToDelete, pc) + delete(bs.pcToBps, pc) + } else { + bs.pcToBps[pc] = bps + } + } + if len(codeBreakpointsToDelete) > 0 { + bs.prog.DeleteBreakpoints(codeBreakpointsToDelete) + } + delete(bs.bpToPCs, bp) +} + +// remove updates rs by removing r, then returns rs. +// The mutex in the BreakpointStore which contains rs should be held. +func remove(rs []*cd.Breakpoint, r *cd.Breakpoint) []*cd.Breakpoint { + for i := range rs { + if rs[i] == r { + rs[i] = rs[len(rs)-1] + rs = rs[0 : len(rs)-1] + return rs + } + } + // We shouldn't reach here. + return rs +} diff --git a/vendor/cloud.google.com/go/cmd/go-cloud-debug-agent/internal/breakpoints/breakpoints_test.go b/vendor/cloud.google.com/go/cmd/go-cloud-debug-agent/internal/breakpoints/breakpoints_test.go new file mode 100644 index 000000000..089a3ba61 --- /dev/null +++ b/vendor/cloud.google.com/go/cmd/go-cloud-debug-agent/internal/breakpoints/breakpoints_test.go @@ -0,0 +1,168 @@ +// Copyright 2016 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package breakpoints + +import ( + "reflect" + "testing" + + "golang.org/x/debug" + cd "google.golang.org/api/clouddebugger/v2" +) + +var ( + testPC1 uint64 = 0x1234 + testPC2 uint64 = 0x5678 + testPC3 uint64 = 0x3333 + testFile = "foo.go" + testLine uint64 = 42 + testLine2 uint64 = 99 + testLogPC uint64 = 0x9abc + testLogLine uint64 = 43 + testBadPC uint64 = 0xdef0 + testBadLine uint64 = 44 + testBP = &cd.Breakpoint{ + Action: "CAPTURE", + Id: "TestBreakpoint", + IsFinalState: false, + Location: &cd.SourceLocation{Path: testFile, Line: int64(testLine)}, + } + testBP2 = &cd.Breakpoint{ + Action: "CAPTURE", + Id: "TestBreakpoint2", + IsFinalState: false, + Location: &cd.SourceLocation{Path: testFile, Line: int64(testLine2)}, + } + testLogBP = &cd.Breakpoint{ + Action: "LOG", + Id: "TestLogBreakpoint", + IsFinalState: false, + Location: &cd.SourceLocation{Path: testFile, Line: int64(testLogLine)}, + } + testBadBP = &cd.Breakpoint{ + Action: "BEEP", + Id: "TestBadBreakpoint", + IsFinalState: false, + Location: &cd.SourceLocation{Path: testFile, Line: int64(testBadLine)}, + } +) + +func TestBreakpointStore(t *testing.T) { + p := &Program{breakpointPCs: make(map[uint64]bool)} + bs := NewBreakpointStore(p) + checkPCs := func(expected map[uint64]bool) { + if !reflect.DeepEqual(p.breakpointPCs, expected) { + t.Errorf("got breakpoint map %v want %v", p.breakpointPCs, expected) + } + } + bs.ProcessBreakpointList([]*cd.Breakpoint{testBP, testBP2, testLogBP, testBadBP}) + checkPCs(map[uint64]bool{ + testPC1: true, + testPC2: true, + testPC3: true, + testLogPC: true, + }) + for _, test := range []struct { + pc uint64 + expected []*cd.Breakpoint + }{ + {testPC1, []*cd.Breakpoint{testBP}}, + {testPC2, []*cd.Breakpoint{testBP}}, + {testPC3, []*cd.Breakpoint{testBP2}}, + {testLogPC, []*cd.Breakpoint{testLogBP}}, + } { + if bps := bs.BreakpointsAtPC(test.pc); !reflect.DeepEqual(bps, test.expected) { + t.Errorf("BreakpointsAtPC(%x): got %v want %v", test.pc, bps, test.expected) + } + } + testBP2.IsFinalState = true + bs.ProcessBreakpointList([]*cd.Breakpoint{testBP, testBP2, testLogBP, testBadBP}) + checkPCs(map[uint64]bool{ + testPC1: true, + testPC2: true, + testPC3: false, + testLogPC: true, + }) + bs.RemoveBreakpoint(testBP) + checkPCs(map[uint64]bool{ + testPC1: false, + testPC2: false, + testPC3: false, + testLogPC: true, + }) + for _, pc := range []uint64{testPC1, testPC2, testPC3} { + if bps := bs.BreakpointsAtPC(pc); len(bps) != 0 { + t.Errorf("BreakpointsAtPC(%x): got %v want []", pc, bps) + } + } + // bs.ErrorBreakpoints should return testBadBP. + errorBps := bs.ErrorBreakpoints() + if len(errorBps) != 1 { + t.Errorf("ErrorBreakpoints: got %d want 1", len(errorBps)) + } else { + bp := errorBps[0] + if bp.Id != testBadBP.Id { + t.Errorf("ErrorBreakpoints: got id %q want 1", bp.Id) + } + if bp.Status == nil || !bp.Status.IsError { + t.Errorf("ErrorBreakpoints: got %v, want error", bp.Status) + } + } + // The error should have been removed by the last call to bs.ErrorBreakpoints. + errorBps = bs.ErrorBreakpoints() + if len(errorBps) != 0 { + t.Errorf("ErrorBreakpoints: got %d want 0", len(errorBps)) + } + // Even if testBadBP is sent in a new list, it should not be returned again. + bs.ProcessBreakpointList([]*cd.Breakpoint{testBadBP}) + errorBps = bs.ErrorBreakpoints() + if len(errorBps) != 0 { + t.Errorf("ErrorBreakpoints: got %d want 0", len(errorBps)) + } +} + +// Program implements the similarly-named interface in x/debug. +// ValueCollector should only call its BreakpointAtLine and DeleteBreakpoints methods. +type Program struct { + debug.Program + // breakpointPCs contains the state of code breakpoints -- true if the + // breakpoint is currently set, false if it has been deleted. + breakpointPCs map[uint64]bool +} + +func (p *Program) BreakpointAtLine(file string, line uint64) ([]uint64, error) { + var pcs []uint64 + switch { + case file == testFile && line == testLine: + pcs = []uint64{testPC1, testPC2} + case file == testFile && line == testLine2: + pcs = []uint64{testPC3} + case file == testFile && line == testLogLine: + pcs = []uint64{testLogPC} + default: + pcs = []uint64{0xbad} + } + for _, pc := range pcs { + p.breakpointPCs[pc] = true + } + return pcs, nil +} + +func (p *Program) DeleteBreakpoints(pcs []uint64) error { + for _, pc := range pcs { + p.breakpointPCs[pc] = false + } + return nil +} diff --git a/vendor/cloud.google.com/go/cmd/go-cloud-debug-agent/internal/controller/client.go b/vendor/cloud.google.com/go/cmd/go-cloud-debug-agent/internal/controller/client.go new file mode 100644 index 000000000..87d283546 --- /dev/null +++ b/vendor/cloud.google.com/go/cmd/go-cloud-debug-agent/internal/controller/client.go @@ -0,0 +1,291 @@ +// Copyright 2016 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package controller is a library for interacting with the Google Cloud Debugger's Debuglet Controller service. +package controller + +import ( + "crypto/sha256" + "encoding/json" + "errors" + "fmt" + "log" + "sync" + + "golang.org/x/net/context" + "golang.org/x/oauth2" + cd "google.golang.org/api/clouddebugger/v2" + "google.golang.org/api/googleapi" + "google.golang.org/api/option" + "google.golang.org/api/transport" +) + +const ( + // agentVersionString identifies the agent to the service. + agentVersionString = "google.com/go-gcp/v0.2" + // initWaitToken is the wait token sent in the first Update request to a server. + initWaitToken = "init" +) + +var ( + // ErrListUnchanged is returned by List if the server time limit is reached + // before the list of breakpoints changes. + ErrListUnchanged = errors.New("breakpoint list unchanged") + // ErrDebuggeeDisabled is returned by List or Update if the server has disabled + // this Debuggee. The caller can retry later. + ErrDebuggeeDisabled = errors.New("debuglet disabled by server") +) + +// Controller manages a connection to the Debuglet Controller service. +type Controller struct { + s serviceInterface + // waitToken is sent with List requests so the server knows which set of + // breakpoints this client has already seen. Each successful List request + // returns a new waitToken to send in the next request. + waitToken string + // verbose determines whether to do some logging + verbose bool + // options, uniquifier and description are used in register. + options Options + uniquifier string + description string + // labels are included when registering the debuggee. They should contain + // the module name, version and minorversion, and are used by the debug UI + // to label the correct version active for debugging. + labels map[string]string + // mu protects debuggeeID + mu sync.Mutex + // debuggeeID is returned from the server on registration, and is passed back + // to the server in List and Update requests. + debuggeeID string +} + +// Options controls how the Debuglet Controller client identifies itself to the server. +// See https://cloud.google.com/storage/docs/projects and +// https://cloud.google.com/tools/cloud-debugger/setting-up-on-compute-engine +// for further documentation of these parameters. +type Options struct { + ProjectNumber string // GCP Project Number. + ProjectID string // GCP Project ID. + AppModule string // Module name for the debugged program. + AppVersion string // Version number for this module. + SourceContexts []*cd.SourceContext // Description of source. + Verbose bool + TokenSource oauth2.TokenSource // Source of Credentials used for Stackdriver Debugger. +} + +type serviceInterface interface { + Register(ctx context.Context, req *cd.RegisterDebuggeeRequest) (*cd.RegisterDebuggeeResponse, error) + Update(ctx context.Context, debuggeeID, breakpointID string, req *cd.UpdateActiveBreakpointRequest) (*cd.UpdateActiveBreakpointResponse, error) + List(ctx context.Context, debuggeeID, waitToken string) (*cd.ListActiveBreakpointsResponse, error) +} + +var newService = func(ctx context.Context, tokenSource oauth2.TokenSource) (serviceInterface, error) { + httpClient, endpoint, err := transport.NewHTTPClient(ctx, option.WithTokenSource(tokenSource)) + if err != nil { + return nil, err + } + s, err := cd.New(httpClient) + if err != nil { + return nil, err + } + if endpoint != "" { + s.BasePath = endpoint + } + return &service{s: s}, nil +} + +type service struct { + s *cd.Service +} + +func (s service) Register(ctx context.Context, req *cd.RegisterDebuggeeRequest) (*cd.RegisterDebuggeeResponse, error) { + call := cd.NewControllerDebuggeesService(s.s).Register(req) + return call.Context(ctx).Do() +} + +func (s service) Update(ctx context.Context, debuggeeID, breakpointID string, req *cd.UpdateActiveBreakpointRequest) (*cd.UpdateActiveBreakpointResponse, error) { + call := cd.NewControllerDebuggeesBreakpointsService(s.s).Update(debuggeeID, breakpointID, req) + return call.Context(ctx).Do() +} + +func (s service) List(ctx context.Context, debuggeeID, waitToken string) (*cd.ListActiveBreakpointsResponse, error) { + call := cd.NewControllerDebuggeesBreakpointsService(s.s).List(debuggeeID) + call.WaitToken(waitToken) + return call.Context(ctx).Do() +} + +// NewController connects to the Debuglet Controller server using the given options, +// and returns a Controller for that connection. +// Google Application Default Credentials are used to connect to the Debuglet Controller; +// see https://developers.google.com/identity/protocols/application-default-credentials +func NewController(ctx context.Context, o Options) (*Controller, error) { + // We build a JSON encoding of o.SourceContexts so we can hash it. + scJSON, err := json.Marshal(o.SourceContexts) + if err != nil { + scJSON = nil + o.SourceContexts = nil + } + const minorversion = "107157" // any arbitrary numeric string + + // Compute a uniquifier string by hashing the project number, app module name, + // app module version, debuglet version, and source context. + // The choice of hash function is arbitrary. + h := sha256.Sum256([]byte(fmt.Sprintf("%d %s %d %s %d %s %d %s %d %s %d %s", + len(o.ProjectNumber), o.ProjectNumber, + len(o.AppModule), o.AppModule, + len(o.AppVersion), o.AppVersion, + len(agentVersionString), agentVersionString, + len(scJSON), scJSON, + len(minorversion), minorversion))) + uniquifier := fmt.Sprintf("%X", h[0:16]) // 32 hex characters + + description := o.ProjectID + if o.AppModule != "" { + description += "-" + o.AppModule + } + if o.AppVersion != "" { + description += "-" + o.AppVersion + } + + s, err := newService(ctx, o.TokenSource) + if err != nil { + return nil, err + } + + // Construct client. + c := &Controller{ + s: s, + waitToken: initWaitToken, + verbose: o.Verbose, + options: o, + uniquifier: uniquifier, + description: description, + labels: map[string]string{ + "module": o.AppModule, + "version": o.AppVersion, + "minorversion": minorversion, + }, + } + + return c, nil +} + +func (c *Controller) getDebuggeeID(ctx context.Context) (string, error) { + c.mu.Lock() + defer c.mu.Unlock() + if c.debuggeeID != "" { + return c.debuggeeID, nil + } + // The debuglet hasn't been registered yet, or it is disabled and we should try registering again. + if err := c.register(ctx); err != nil { + return "", err + } + return c.debuggeeID, nil +} + +// List retrieves the current list of breakpoints from the server. +// If the set of breakpoints on the server is the same as the one returned in +// the previous call to List, the server can delay responding until it changes, +// and return an error instead if no change occurs before a time limit the +// server sets. List can't be called concurrently with itself. +func (c *Controller) List(ctx context.Context) (*cd.ListActiveBreakpointsResponse, error) { + id, err := c.getDebuggeeID(ctx) + if err != nil { + return nil, err + } + resp, err := c.s.List(ctx, id, c.waitToken) + if err != nil { + if isAbortedError(err) { + return nil, ErrListUnchanged + } + // For other errors, the protocol requires that we attempt to re-register. + c.mu.Lock() + defer c.mu.Unlock() + if regError := c.register(ctx); regError != nil { + return nil, regError + } + return nil, err + } + if resp == nil { + return nil, errors.New("no response") + } + if c.verbose { + log.Printf("List response: %v", resp) + } + c.waitToken = resp.NextWaitToken + return resp, nil +} + +// isAbortedError tests if err is a *googleapi.Error, that it contains one error +// in Errors, and that that error's Reason is "aborted". +func isAbortedError(err error) bool { + e, _ := err.(*googleapi.Error) + if e == nil { + return false + } + if len(e.Errors) != 1 { + return false + } + return e.Errors[0].Reason == "aborted" +} + +// Update reports information to the server about a breakpoint that was hit. +// Update can be called concurrently with List and Update. +func (c *Controller) Update(ctx context.Context, breakpointID string, bp *cd.Breakpoint) error { + req := &cd.UpdateActiveBreakpointRequest{Breakpoint: bp} + if c.verbose { + log.Printf("sending update for %s: %v", breakpointID, req) + } + id, err := c.getDebuggeeID(ctx) + if err != nil { + return err + } + _, err = c.s.Update(ctx, id, breakpointID, req) + return err +} + +// register calls the Debuglet Controller Register method, and sets c.debuggeeID. +// c.mu should be locked while calling this function. List and Update can't +// make progress until it returns. +func (c *Controller) register(ctx context.Context) error { + req := cd.RegisterDebuggeeRequest{ + Debuggee: &cd.Debuggee{ + AgentVersion: agentVersionString, + Description: c.description, + Project: c.options.ProjectNumber, + SourceContexts: c.options.SourceContexts, + Uniquifier: c.uniquifier, + Labels: c.labels, + }, + } + resp, err := c.s.Register(ctx, &req) + if err != nil { + return err + } + if resp == nil { + return errors.New("register: no response") + } + if resp.Debuggee.IsDisabled { + // Setting c.debuggeeID to empty makes sure future List and Update calls + // will call register first. + c.debuggeeID = "" + } else { + c.debuggeeID = resp.Debuggee.Id + } + if c.debuggeeID == "" { + return ErrDebuggeeDisabled + } + return nil +} diff --git a/vendor/cloud.google.com/go/cmd/go-cloud-debug-agent/internal/controller/client_test.go b/vendor/cloud.google.com/go/cmd/go-cloud-debug-agent/internal/controller/client_test.go new file mode 100644 index 000000000..fa0634706 --- /dev/null +++ b/vendor/cloud.google.com/go/cmd/go-cloud-debug-agent/internal/controller/client_test.go @@ -0,0 +1,254 @@ +// Copyright 2016 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package controller + +import ( + "bytes" + "errors" + "fmt" + "strconv" + "testing" + + "golang.org/x/net/context" + "golang.org/x/oauth2" + + cd "google.golang.org/api/clouddebugger/v2" + "google.golang.org/api/googleapi" +) + +const ( + testDebuggeeID = "d12345" + testBreakpointID = "bp12345" +) + +var ( + // The sequence of wait tokens in List requests and responses. + expectedWaitToken = []string{"init", "token1", "token2", "token1", "token1"} + // The set of breakpoints returned from each List call. + expectedBreakpoints = [][]*cd.Breakpoint{ + nil, + { + &cd.Breakpoint{ + Id: testBreakpointID, + IsFinalState: false, + Location: &cd.SourceLocation{Line: 42, Path: "foo.go"}, + }, + }, + nil, + } + abortedError error = &googleapi.Error{ + Code: 409, + Message: "Conflict", + Body: `{ + "error": { + "errors": [ + { + "domain": "global", + "reason": "aborted", + "message": "Conflict" + } + ], + "code": 409, + "message": "Conflict" + } + }`, + Errors: []googleapi.ErrorItem{ + {Reason: "aborted", Message: "Conflict"}, + }, + } + backendError error = &googleapi.Error{ + Code: 503, + Message: "Backend Error", + Body: `{ + "error": { + "errors": [ + { + "domain": "global", + "reason": "backendError", + "message": "Backend Error" + } + ], + "code": 503, + "message": "Backend Error" + } + }`, + Errors: []googleapi.ErrorItem{ + {Reason: "backendError", Message: "Backend Error"}, + }, + } +) + +type mockService struct { + t *testing.T + listCallsSeen int + registerCallsSeen int +} + +func (s *mockService) Register(ctx context.Context, req *cd.RegisterDebuggeeRequest) (*cd.RegisterDebuggeeResponse, error) { + s.registerCallsSeen++ + if req.Debuggee == nil { + s.t.Errorf("missing debuggee") + return nil, nil + } + if req.Debuggee.AgentVersion == "" { + s.t.Errorf("missing agent version") + } + if req.Debuggee.Description == "" { + s.t.Errorf("missing debuglet description") + } + if req.Debuggee.Project == "" { + s.t.Errorf("missing project id") + } + if req.Debuggee.Uniquifier == "" { + s.t.Errorf("missing uniquifier") + } + return &cd.RegisterDebuggeeResponse{ + Debuggee: &cd.Debuggee{Id: testDebuggeeID}, + }, nil +} + +func (s *mockService) Update(ctx context.Context, id, breakpointID string, req *cd.UpdateActiveBreakpointRequest) (*cd.UpdateActiveBreakpointResponse, error) { + if id != testDebuggeeID { + s.t.Errorf("got debuggee ID %s want %s", id, testDebuggeeID) + } + if breakpointID != testBreakpointID { + s.t.Errorf("got breakpoint ID %s want %s", breakpointID, testBreakpointID) + } + if !req.Breakpoint.IsFinalState { + s.t.Errorf("got IsFinalState = false, want true") + } + return nil, nil +} + +func (s *mockService) List(ctx context.Context, id, waitToken string) (*cd.ListActiveBreakpointsResponse, error) { + if id != testDebuggeeID { + s.t.Errorf("got debuggee ID %s want %s", id, testDebuggeeID) + } + if waitToken != expectedWaitToken[s.listCallsSeen] { + s.t.Errorf("got wait token %s want %s", waitToken, expectedWaitToken[s.listCallsSeen]) + } + s.listCallsSeen++ + if s.listCallsSeen == 4 { + return nil, backendError + } + if s.listCallsSeen == 5 { + return nil, abortedError + } + resp := &cd.ListActiveBreakpointsResponse{ + Breakpoints: expectedBreakpoints[s.listCallsSeen-1], + NextWaitToken: expectedWaitToken[s.listCallsSeen], + } + return resp, nil +} + +func TestDebugletControllerClientLibrary(t *testing.T) { + var ( + m *mockService + c *Controller + list *cd.ListActiveBreakpointsResponse + err error + ) + m = &mockService{t: t} + newService = func(context.Context, oauth2.TokenSource) (serviceInterface, error) { return m, nil } + opts := Options{ + ProjectNumber: "5", + ProjectID: "p1", + AppModule: "mod1", + AppVersion: "v1", + } + ctx := context.Background() + if c, err = NewController(ctx, opts); err != nil { + t.Fatal("Initializing Controller client:", err) + } + if err := validateLabels(c, opts); err != nil { + t.Fatalf("Invalid labels:\n%v", err) + } + if list, err = c.List(ctx); err != nil { + t.Fatal("List:", err) + } + if m.registerCallsSeen != 1 { + t.Errorf("saw %d Register calls, want 1", m.registerCallsSeen) + } + if list, err = c.List(ctx); err != nil { + t.Fatal("List:", err) + } + if len(list.Breakpoints) != 1 { + t.Fatalf("got %d breakpoints, want 1", len(list.Breakpoints)) + } + if err = c.Update(ctx, list.Breakpoints[0].Id, &cd.Breakpoint{Id: testBreakpointID, IsFinalState: true}); err != nil { + t.Fatal("Update:", err) + } + if list, err = c.List(ctx); err != nil { + t.Fatal("List:", err) + } + if m.registerCallsSeen != 1 { + t.Errorf("saw %d Register calls, want 1", m.registerCallsSeen) + } + // The next List call produces an error that should cause a Register call. + if list, err = c.List(ctx); err == nil { + t.Fatal("List should have returned an error") + } + if m.registerCallsSeen != 2 { + t.Errorf("saw %d Register calls, want 2", m.registerCallsSeen) + } + // The next List call produces an error that should not cause a Register call. + if list, err = c.List(ctx); err == nil { + t.Fatal("List should have returned an error") + } + if m.registerCallsSeen != 2 { + t.Errorf("saw %d Register calls, want 2", m.registerCallsSeen) + } + if m.listCallsSeen != 5 { + t.Errorf("saw %d list calls, want 5", m.listCallsSeen) + } +} + +func validateLabels(c *Controller, o Options) error { + errMsg := new(bytes.Buffer) + if m, ok := c.labels["module"]; ok { + if m != o.AppModule { + errMsg.WriteString(fmt.Sprintf("label module: want %s, got %s\n", o.AppModule, m)) + } + } else { + errMsg.WriteString("Missing \"module\" label\n") + } + if v, ok := c.labels["version"]; ok { + if v != o.AppVersion { + errMsg.WriteString(fmt.Sprintf("label version: want %s, got %s\n", o.AppVersion, v)) + } + } else { + errMsg.WriteString("Missing \"version\" label\n") + } + if mv, ok := c.labels["minorversion"]; ok { + if _, err := strconv.Atoi(mv); err != nil { + errMsg.WriteString(fmt.Sprintln("label minorversion: not a numeric string:", mv)) + } + } else { + errMsg.WriteString("Missing \"minorversion\" label\n") + } + if errMsg.Len() != 0 { + return errors.New(errMsg.String()) + } + return nil +} + +func TestIsAbortedError(t *testing.T) { + if !isAbortedError(abortedError) { + t.Errorf("isAborted(%+v): got false, want true", abortedError) + } + if isAbortedError(backendError) { + t.Errorf("isAborted(%+v): got true, want false", backendError) + } +} diff --git a/vendor/cloud.google.com/go/cmd/go-cloud-debug-agent/internal/valuecollector/valuecollector.go b/vendor/cloud.google.com/go/cmd/go-cloud-debug-agent/internal/valuecollector/valuecollector.go new file mode 100644 index 000000000..8dadc2f63 --- /dev/null +++ b/vendor/cloud.google.com/go/cmd/go-cloud-debug-agent/internal/valuecollector/valuecollector.go @@ -0,0 +1,460 @@ +// Copyright 2016 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package valuecollector is used to collect the values of variables in a program. +package valuecollector + +import ( + "bytes" + "fmt" + "strconv" + "strings" + + "golang.org/x/debug" + cd "google.golang.org/api/clouddebugger/v2" +) + +const ( + maxArrayLength = 50 + maxMapLength = 20 +) + +// Collector is given references to variables from a program being debugged +// using AddVariable. Then when ReadValues is called, the Collector will fetch +// the values of those variables. Any variables referred to by those values +// will also be fetched; e.g. the targets of pointers, members of structs, +// elements of slices, etc. This continues iteratively, building a graph of +// values, until all the reachable values are fetched, or a size limit is +// reached. +// +// Variables are passed to the Collector as debug.Var, which is used by x/debug +// to represent references to variables. Values are returned as cd.Variable, +// which is used by the Debuglet Controller to represent the graph of values. +// +// For example, if the program has a struct variable: +// +// foo := SomeStruct{a:42, b:"xyz"} +// +// and we call AddVariable with a reference to foo, we will get back a result +// like: +// +// cd.Variable{Name:"foo", VarTableIndex:10} +// +// which denotes a variable named "foo" which will have its value stored in +// element 10 of the table that will later be returned by ReadValues. That +// element might be: +// +// out[10] = &cd.Variable{Members:{{Name:"a", VarTableIndex:11},{Name:"b", VarTableIndex:12}}} +// +// which denotes a struct with two members a and b, whose values are in elements +// 11 and 12 of the output table: +// +// out[11] = &cd.Variable{Value:"42"} +// out[12] = &cd.Variable{Value:"xyz"} +type Collector struct { + // prog is the program being debugged. + prog debug.Program + // limit is the maximum size of the output slice of values. + limit int + // index is a map from references (variables and map elements) to their + // locations in the table. + index map[reference]int + // table contains the references, including those given to the + // Collector directly and those the Collector itself found. + // If VarTableIndex is set to 0 in a cd.Variable, it is ignored, so the first entry + // of table can't be used. On initialization we put a dummy value there. + table []reference +} + +// reference represents a value which is in the queue to be read by the +// collector. It is either a debug.Var, or a mapElement. +type reference interface{} + +// mapElement represents an element of a map in the debugged program's memory. +type mapElement struct { + debug.Map + index uint64 +} + +// NewCollector returns a Collector for the given program and size limit. +// The limit is the maximum size of the slice of values returned by ReadValues. +func NewCollector(prog debug.Program, limit int) *Collector { + return &Collector{ + prog: prog, + limit: limit, + index: make(map[reference]int), + table: []reference{debug.Var{}}, + } +} + +// AddVariable adds another variable to be collected. +// The Collector doesn't get the value immediately; it returns a cd.Variable +// that contains an index into the table which will later be returned by +// ReadValues. +func (c *Collector) AddVariable(lv debug.LocalVar) *cd.Variable { + ret := &cd.Variable{Name: lv.Name} + if index, ok := c.add(lv.Var); !ok { + // If the add call failed, it's because we reached the size limit. + // The Debuglet Controller's convention is to pass it a "Not Captured" error + // in this case. + ret.Status = statusMessage(messageNotCaptured, true, refersToVariableName) + } else { + ret.VarTableIndex = int64(index) + } + return ret +} + +// add adds a reference to the set of values to be read from the +// program. It returns the index in the output table that will contain the +// corresponding value. It fails if the table has reached the size limit. +// It deduplicates references, so the index may be the same as one that was +// returned from an earlier add call. +func (c *Collector) add(r reference) (outputIndex int, ok bool) { + if i, ok := c.index[r]; ok { + return i, true + } + i := len(c.table) + if i >= c.limit { + return 0, false + } + c.index[r] = i + c.table = append(c.table, r) + return i, true +} + +func addMember(v *cd.Variable, name string) *cd.Variable { + v2 := &cd.Variable{Name: name} + v.Members = append(v.Members, v2) + return v2 +} + +// ReadValues fetches values of the variables that were passed to the Collector +// with AddVariable. The values of any new variables found are also fetched, +// e.g. the targets of pointers or the members of structs, until we reach the +// size limit or we run out of values to fetch. +// The results are output as a []*cd.Variable, which is the type we need to send +// to the Debuglet Controller after we trigger a breakpoint. +func (c *Collector) ReadValues() (out []*cd.Variable) { + for i := 0; i < len(c.table); i++ { + // Create a new cd.Variable for this value, and append it to the output. + dcv := new(cd.Variable) + out = append(out, dcv) + if i == 0 { + // The first element is unused. + continue + } + switch x := c.table[i].(type) { + case mapElement: + key, value, err := c.prog.MapElement(x.Map, x.index) + if err != nil { + dcv.Status = statusMessage(err.Error(), true, refersToVariableValue) + continue + } + // Add a member for the key. + member := addMember(dcv, "key") + if index, ok := c.add(key); !ok { + // The table is full. + member.Status = statusMessage(messageNotCaptured, true, refersToVariableName) + continue + } else { + member.VarTableIndex = int64(index) + } + // Add a member for the value. + member = addMember(dcv, "value") + if index, ok := c.add(value); !ok { + // The table is full. + member.Status = statusMessage(messageNotCaptured, true, refersToVariableName) + } else { + member.VarTableIndex = int64(index) + } + case debug.Var: + if v, err := c.prog.Value(x); err != nil { + dcv.Status = statusMessage(err.Error(), true, refersToVariableValue) + } else { + c.FillValue(v, dcv) + } + } + } + return out +} + +// indexable is an interface for arrays, slices and channels. +type indexable interface { + Len() uint64 + Element(uint64) debug.Var +} + +// channel implements indexable. +type channel struct { + debug.Channel +} + +func (c channel) Len() uint64 { + return c.Length +} + +var ( + _ indexable = debug.Array{} + _ indexable = debug.Slice{} + _ indexable = channel{} +) + +// FillValue copies a value into a cd.Variable. Any variables referred to by +// that value, e.g. struct members and pointer targets, are added to the +// collector's queue, to be fetched later by ReadValues. +func (c *Collector) FillValue(v debug.Value, dcv *cd.Variable) { + if c, ok := v.(debug.Channel); ok { + // Convert to channel, which implements indexable. + v = channel{c} + } + // Fill in dcv in a manner depending on the type of the value we got. + switch val := v.(type) { + case int8, int16, int32, int64, bool, uint8, uint16, uint32, uint64, float32, float64, complex64, complex128: + // For simple types, we just print the value to dcv.Value. + dcv.Value = fmt.Sprint(val) + case string: + // Put double quotes around strings. + dcv.Value = strconv.Quote(val) + case debug.String: + if uint64(len(val.String)) < val.Length { + // This string value was truncated. + dcv.Value = strconv.Quote(val.String + "...") + } else { + dcv.Value = strconv.Quote(val.String) + } + case debug.Struct: + // For structs, we add an entry to dcv.Members for each field in the + // struct. + // Each member will contain the name of the field, and the index in the + // output table which will contain the value of that field. + for _, f := range val.Fields { + member := addMember(dcv, f.Name) + if index, ok := c.add(f.Var); !ok { + // The table is full. + member.Status = statusMessage(messageNotCaptured, true, refersToVariableName) + } else { + member.VarTableIndex = int64(index) + } + } + case debug.Map: + dcv.Value = fmt.Sprintf("len = %d", val.Length) + for i := uint64(0); i < val.Length; i++ { + field := addMember(dcv, `⚫`) + if i == maxMapLength { + field.Name = "..." + field.Status = statusMessage(messageTruncated, true, refersToVariableName) + break + } + if index, ok := c.add(mapElement{val, i}); !ok { + // The value table is full; add a member to contain the error message. + field.Name = "..." + field.Status = statusMessage(messageNotCaptured, true, refersToVariableName) + break + } else { + field.VarTableIndex = int64(index) + } + } + case debug.Pointer: + if val.Address == 0 { + dcv.Value = "<nil>" + } else if val.TypeID == 0 { + // We don't know the type of the pointer, so just output the address as + // the value. + dcv.Value = fmt.Sprintf("0x%X", val.Address) + dcv.Status = statusMessage(messageUnknownPointerType, false, refersToVariableName) + } else { + // Adds the pointed-to variable to the table, and links this value to + // that table entry through VarTableIndex. + dcv.Value = fmt.Sprintf("0x%X", val.Address) + target := addMember(dcv, "") + if index, ok := c.add(debug.Var(val)); !ok { + target.Status = statusMessage(messageNotCaptured, true, refersToVariableName) + } else { + target.VarTableIndex = int64(index) + } + } + case indexable: + // Arrays, slices and channels. + dcv.Value = "len = " + fmt.Sprint(val.Len()) + for j := uint64(0); j < val.Len(); j++ { + field := addMember(dcv, fmt.Sprint(`[`, j, `]`)) + if j == maxArrayLength { + field.Name = "..." + field.Status = statusMessage(messageTruncated, true, refersToVariableName) + break + } + vr := val.Element(j) + if index, ok := c.add(vr); !ok { + // The value table is full; add a member to contain the error message. + field.Name = "..." + field.Status = statusMessage(messageNotCaptured, true, refersToVariableName) + break + } else { + // Add a member with the index as the name. + field.VarTableIndex = int64(index) + } + } + default: + dcv.Status = statusMessage(messageUnknownType, false, refersToVariableName) + } +} + +// statusMessage returns a *cd.StatusMessage with the given message, IsError +// field and refersTo field. +func statusMessage(msg string, isError bool, refersTo int) *cd.StatusMessage { + return &cd.StatusMessage{ + Description: &cd.FormatMessage{Format: "$0", Parameters: []string{msg}}, + IsError: isError, + RefersTo: refersToString[refersTo], + } +} + +// LogString produces a string for a logpoint, substituting in variable values +// using evaluatedExpressions and varTable. +func LogString(s string, evaluatedExpressions []*cd.Variable, varTable []*cd.Variable) string { + var buf bytes.Buffer + fmt.Fprintf(&buf, "LOGPOINT: ") + seen := make(map[*cd.Variable]bool) + for i := 0; i < len(s); { + if s[i] == '$' { + i++ + if num, n, ok := parseToken(s[i:], len(evaluatedExpressions)-1); ok { + // This token is one of $0, $1, etc. Write the corresponding expression. + writeExpression(&buf, evaluatedExpressions[num], false, varTable, seen) + i += n + } else { + // Something else, like $$. + buf.WriteByte(s[i]) + i++ + } + } else { + buf.WriteByte(s[i]) + i++ + } + } + return buf.String() +} + +func parseToken(s string, max int) (num int, bytesRead int, ok bool) { + var i int + for i < len(s) && s[i] >= '0' && s[i] <= '9' { + i++ + } + num, err := strconv.Atoi(s[:i]) + return num, i, err == nil && num <= max +} + +// writeExpression recursively writes variables to buf, in a format suitable +// for logging. If printName is true, writes the name of the variable. +func writeExpression(buf *bytes.Buffer, v *cd.Variable, printName bool, varTable []*cd.Variable, seen map[*cd.Variable]bool) { + if v == nil { + // Shouldn't happen. + return + } + name, value, status, members := v.Name, v.Value, v.Status, v.Members + + // If v.VarTableIndex is not zero, it refers to an element of varTable. + // We merge its fields with the fields we got from v. + var other *cd.Variable + if idx := int(v.VarTableIndex); idx > 0 && idx < len(varTable) { + other = varTable[idx] + } + if other != nil { + if name == "" { + name = other.Name + } + if value == "" { + value = other.Value + } + if status == nil { + status = other.Status + } + if len(members) == 0 { + members = other.Members + } + } + if printName && name != "" { + buf.WriteString(name) + buf.WriteByte(':') + } + + // If we have seen this value before, write "..." rather than repeating it. + if seen[v] { + buf.WriteString("...") + return + } + seen[v] = true + if other != nil { + if seen[other] { + buf.WriteString("...") + return + } + seen[other] = true + } + + if value != "" && !strings.HasPrefix(value, "len = ") { + // A plain value. + buf.WriteString(value) + } else if status != nil && status.Description != nil { + // An error. + for _, p := range status.Description.Parameters { + buf.WriteByte('(') + buf.WriteString(p) + buf.WriteByte(')') + } + } else if name == `⚫` { + // A map element. + first := true + for _, member := range members { + if first { + first = false + } else { + buf.WriteByte(':') + } + writeExpression(buf, member, false, varTable, seen) + } + } else { + // A map, array, slice, channel, or struct. + isStruct := value == "" + first := true + buf.WriteByte('{') + for _, member := range members { + if first { + first = false + } else { + buf.WriteString(", ") + } + writeExpression(buf, member, isStruct, varTable, seen) + } + buf.WriteByte('}') + } +} + +const ( + // Error messages for cd.StatusMessage + messageNotCaptured = "Not captured" + messageTruncated = "Truncated" + messageUnknownPointerType = "Unknown pointer type" + messageUnknownType = "Unknown type" + // RefersTo values for cd.StatusMessage. + refersToVariableName = iota + refersToVariableValue +) + +// refersToString contains the strings for each refersTo value. +// See the definition of StatusMessage in the v2/clouddebugger package. +var refersToString = map[int]string{ + refersToVariableName: "VARIABLE_NAME", + refersToVariableValue: "VARIABLE_VALUE", +} diff --git a/vendor/cloud.google.com/go/cmd/go-cloud-debug-agent/internal/valuecollector/valuecollector_test.go b/vendor/cloud.google.com/go/cmd/go-cloud-debug-agent/internal/valuecollector/valuecollector_test.go new file mode 100644 index 000000000..2bc97dcfa --- /dev/null +++ b/vendor/cloud.google.com/go/cmd/go-cloud-debug-agent/internal/valuecollector/valuecollector_test.go @@ -0,0 +1,418 @@ +// Copyright 2016 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package valuecollector + +import ( + "fmt" + "reflect" + "testing" + + "golang.org/x/debug" + cd "google.golang.org/api/clouddebugger/v2" +) + +const ( + // Some arbitrary type IDs for the test, for use in debug.Var's TypeID field. + // A TypeID of 0 means the type is unknown, so we start at 1. + int16Type = iota + 1 + stringType + structType + pointerType + arrayType + int32Type + debugStringType + mapType + channelType + sliceType +) + +func TestValueCollector(t *testing.T) { + // Construct the collector. + c := NewCollector(&Program{}, 26) + // Add some variables of various types, whose values we want the collector to read. + variablesToAdd := []debug.LocalVar{ + {Name: "a", Var: debug.Var{int16Type, 0x1}}, + {Name: "b", Var: debug.Var{stringType, 0x2}}, + {Name: "c", Var: debug.Var{structType, 0x3}}, + {Name: "d", Var: debug.Var{pointerType, 0x4}}, + {Name: "e", Var: debug.Var{arrayType, 0x5}}, + {Name: "f", Var: debug.Var{debugStringType, 0x6}}, + {Name: "g", Var: debug.Var{mapType, 0x7}}, + {Name: "h", Var: debug.Var{channelType, 0x8}}, + {Name: "i", Var: debug.Var{sliceType, 0x9}}, + } + expectedResults := []*cd.Variable{ + &cd.Variable{Name: "a", VarTableIndex: 1}, + &cd.Variable{Name: "b", VarTableIndex: 2}, + &cd.Variable{Name: "c", VarTableIndex: 3}, + &cd.Variable{Name: "d", VarTableIndex: 4}, + &cd.Variable{Name: "e", VarTableIndex: 5}, + &cd.Variable{Name: "f", VarTableIndex: 6}, + &cd.Variable{Name: "g", VarTableIndex: 7}, + &cd.Variable{Name: "h", VarTableIndex: 8}, + &cd.Variable{Name: "i", VarTableIndex: 9}, + } + for i, v := range variablesToAdd { + added := c.AddVariable(v) + if !reflect.DeepEqual(added, expectedResults[i]) { + t.Errorf("AddVariable: got %+v want %+v", *added, *expectedResults[i]) + } + } + // Read the values, compare the output to what we expect. + v := c.ReadValues() + expectedValues := []*cd.Variable{ + &cd.Variable{}, + &cd.Variable{Value: "1"}, + &cd.Variable{Value: `"hello"`}, + &cd.Variable{ + Members: []*cd.Variable{ + &cd.Variable{Name: "x", VarTableIndex: 1}, + &cd.Variable{Name: "y", VarTableIndex: 2}, + }, + }, + &cd.Variable{ + Members: []*cd.Variable{ + &cd.Variable{VarTableIndex: 1}, + }, + Value: "0x1", + }, + &cd.Variable{ + Members: []*cd.Variable{ + &cd.Variable{Name: "[0]", VarTableIndex: 10}, + &cd.Variable{Name: "[1]", VarTableIndex: 11}, + &cd.Variable{Name: "[2]", VarTableIndex: 12}, + &cd.Variable{Name: "[3]", VarTableIndex: 13}, + }, + Value: "len = 4", + }, + &cd.Variable{Value: `"world"`}, + &cd.Variable{ + Members: []*cd.Variable{ + &cd.Variable{Name: "⚫", VarTableIndex: 14}, + &cd.Variable{Name: "⚫", VarTableIndex: 15}, + &cd.Variable{Name: "⚫", VarTableIndex: 16}, + }, + Value: "len = 3", + }, + &cd.Variable{ + Members: []*cd.Variable{ + &cd.Variable{Name: "[0]", VarTableIndex: 17}, + &cd.Variable{Name: "[1]", VarTableIndex: 18}, + }, + Value: "len = 2", + }, + &cd.Variable{ + Members: []*cd.Variable{ + &cd.Variable{Name: "[0]", VarTableIndex: 19}, + &cd.Variable{Name: "[1]", VarTableIndex: 20}, + }, + Value: "len = 2", + }, + &cd.Variable{Value: "100"}, + &cd.Variable{Value: "104"}, + &cd.Variable{Value: "108"}, + &cd.Variable{Value: "112"}, + &cd.Variable{ + Members: []*cd.Variable{ + &cd.Variable{Name: "key", VarTableIndex: 21}, + &cd.Variable{Name: "value", VarTableIndex: 22}, + }, + }, + &cd.Variable{ + Members: []*cd.Variable{ + &cd.Variable{Name: "key", VarTableIndex: 23}, + &cd.Variable{Name: "value", VarTableIndex: 24}, + }, + }, + &cd.Variable{ + Members: []*cd.Variable{ + &cd.Variable{Name: "key", VarTableIndex: 25}, + &cd.Variable{ + Name: "value", + Status: &cd.StatusMessage{ + Description: &cd.FormatMessage{ + Format: "$0", + Parameters: []string{"Not captured"}, + }, + IsError: true, + RefersTo: "VARIABLE_NAME", + }, + }, + }, + }, + &cd.Variable{Value: "246"}, + &cd.Variable{Value: "210"}, + &cd.Variable{Value: "300"}, + &cd.Variable{Value: "304"}, + &cd.Variable{Value: "400"}, + &cd.Variable{Value: "404"}, + &cd.Variable{Value: "1400"}, + &cd.Variable{Value: "1404"}, + &cd.Variable{Value: "2400"}, + } + if !reflect.DeepEqual(v, expectedValues) { + t.Errorf("ReadValues: got %v want %v", v, expectedValues) + // Do element-by-element comparisons, for more useful error messages. + for i := range v { + if i < len(expectedValues) && !reflect.DeepEqual(v[i], expectedValues[i]) { + t.Errorf("element %d: got %+v want %+v", i, *v[i], *expectedValues[i]) + } + } + } +} + +// Program implements the similarly-named interface in x/debug. +// ValueCollector should only call its Value and MapElement methods. +type Program struct { + debug.Program +} + +func (p *Program) Value(v debug.Var) (debug.Value, error) { + // We determine what to return using v.TypeID. + switch v.TypeID { + case int16Type: + // We use the address as the value, so that we're testing whether the right + // address was calculated. + return int16(v.Address), nil + case stringType: + // A string. + return "hello", nil + case structType: + // A struct with two elements. + return debug.Struct{ + Fields: []debug.StructField{ + { + Name: "x", + Var: debug.Var{int16Type, 0x1}, + }, + { + Name: "y", + Var: debug.Var{stringType, 0x2}, + }, + }, + }, nil + case pointerType: + // A pointer to the first variable above. + return debug.Pointer{int16Type, 0x1}, nil + case arrayType: + // An array of 4 32-bit-wide elements. + return debug.Array{ + ElementTypeID: int32Type, + Address: 0x64, + Length: 4, + StrideBits: 32, + }, nil + case debugStringType: + return debug.String{ + Length: 5, + String: "world", + }, nil + case mapType: + return debug.Map{ + TypeID: 99, + Address: 0x100, + Length: 3, + }, nil + case channelType: + return debug.Channel{ + ElementTypeID: int32Type, + Address: 200, + Buffer: 210, + Length: 2, + Capacity: 10, + Stride: 4, + BufferStart: 9, + }, nil + case sliceType: + // A slice of 2 32-bit-wide elements. + return debug.Slice{ + Array: debug.Array{ + ElementTypeID: int32Type, + Address: 300, + Length: 2, + StrideBits: 32, + }, + Capacity: 50, + }, nil + case int32Type: + // We use the address as the value, so that we're testing whether the right + // address was calculated. + return int32(v.Address), nil + } + return nil, fmt.Errorf("unexpected Value request") +} + +func (p *Program) MapElement(m debug.Map, index uint64) (debug.Var, debug.Var, error) { + return debug.Var{TypeID: int16Type, Address: 1000*index + 400}, + debug.Var{TypeID: int32Type, Address: 1000*index + 404}, + nil +} + +func TestLogString(t *testing.T) { + bp := cd.Breakpoint{ + Action: "LOG", + LogMessageFormat: "$0 hello, $$7world! $1 $2 $3 $4 $5$6 $7 $8", + EvaluatedExpressions: []*cd.Variable{ + &cd.Variable{Name: "a", VarTableIndex: 1}, + &cd.Variable{Name: "b", VarTableIndex: 2}, + &cd.Variable{Name: "c", VarTableIndex: 3}, + &cd.Variable{Name: "d", VarTableIndex: 4}, + &cd.Variable{Name: "e", VarTableIndex: 5}, + &cd.Variable{Name: "f", VarTableIndex: 6}, + &cd.Variable{Name: "g", VarTableIndex: 7}, + &cd.Variable{Name: "h", VarTableIndex: 8}, + &cd.Variable{Name: "i", VarTableIndex: 9}, + }, + } + varTable := []*cd.Variable{ + &cd.Variable{}, + &cd.Variable{Value: "1"}, + &cd.Variable{Value: `"hello"`}, + &cd.Variable{ + Members: []*cd.Variable{ + &cd.Variable{Name: "x", Value: "1"}, + &cd.Variable{Name: "y", Value: `"hello"`}, + &cd.Variable{Name: "z", VarTableIndex: 3}, + }, + }, + &cd.Variable{ + Members: []*cd.Variable{ + &cd.Variable{VarTableIndex: 1}, + }, + Value: "0x1", + }, + &cd.Variable{ + Members: []*cd.Variable{ + &cd.Variable{Name: "[0]", VarTableIndex: 10}, + &cd.Variable{Name: "[1]", VarTableIndex: 11}, + &cd.Variable{Name: "[2]", VarTableIndex: 12}, + &cd.Variable{Name: "[3]", VarTableIndex: 13}, + }, + Value: "len = 4", + }, + &cd.Variable{Value: `"world"`}, + &cd.Variable{ + Members: []*cd.Variable{ + &cd.Variable{Name: "⚫", VarTableIndex: 14}, + &cd.Variable{Name: "⚫", VarTableIndex: 15}, + &cd.Variable{Name: "⚫", VarTableIndex: 16}, + }, + Value: "len = 3", + }, + &cd.Variable{ + Members: []*cd.Variable{ + &cd.Variable{Name: "[0]", VarTableIndex: 17}, + &cd.Variable{Name: "[1]", VarTableIndex: 18}, + }, + Value: "len = 2", + }, + &cd.Variable{ + Members: []*cd.Variable{ + &cd.Variable{Name: "[0]", VarTableIndex: 19}, + &cd.Variable{Name: "[1]", VarTableIndex: 20}, + }, + Value: "len = 2", + }, + &cd.Variable{Value: "100"}, + &cd.Variable{Value: "104"}, + &cd.Variable{Value: "108"}, + &cd.Variable{Value: "112"}, + &cd.Variable{ + Members: []*cd.Variable{ + &cd.Variable{Name: "key", VarTableIndex: 21}, + &cd.Variable{Name: "value", VarTableIndex: 22}, + }, + }, + &cd.Variable{ + Members: []*cd.Variable{ + &cd.Variable{Name: "key", VarTableIndex: 23}, + &cd.Variable{Name: "value", VarTableIndex: 24}, + }, + }, + &cd.Variable{ + Members: []*cd.Variable{ + &cd.Variable{Name: "key", VarTableIndex: 25}, + &cd.Variable{ + Name: "value", + Status: &cd.StatusMessage{ + Description: &cd.FormatMessage{ + Format: "$0", + Parameters: []string{"Not captured"}, + }, + IsError: true, + RefersTo: "VARIABLE_NAME", + }, + }, + }, + }, + &cd.Variable{Value: "246"}, + &cd.Variable{Value: "210"}, + &cd.Variable{Value: "300"}, + &cd.Variable{Value: "304"}, + &cd.Variable{Value: "400"}, + &cd.Variable{Value: "404"}, + &cd.Variable{Value: "1400"}, + &cd.Variable{Value: "1404"}, + &cd.Variable{Value: "2400"}, + } + s := LogString(bp.LogMessageFormat, bp.EvaluatedExpressions, varTable) + expected := `LOGPOINT: 1 hello, $7world! "hello" {x:1, y:"hello", z:...} ` + + `0x1 {100, 104, 108, 112} "world"{400:404, 1400:1404, 2400:(Not captured)} ` + + `{246, 210} {300, 304}` + if s != expected { + t.Errorf("LogString: got %q want %q", s, expected) + } +} + +func TestParseToken(t *testing.T) { + for _, c := range []struct { + s string + max int + num int + n int + ok bool + }{ + {"", 0, 0, 0, false}, + {".", 0, 0, 0, false}, + {"0", 0, 0, 1, true}, + {"0", 1, 0, 1, true}, + {"00", 0, 0, 2, true}, + {"1.", 1, 1, 1, true}, + {"1.", 0, 0, 0, false}, + {"10", 10, 10, 2, true}, + {"10..", 10, 10, 2, true}, + {"10", 11, 10, 2, true}, + {"10..", 11, 10, 2, true}, + {"10", 9, 0, 0, false}, + {"10..", 9, 0, 0, false}, + {" 10", 10, 0, 0, false}, + {"010", 10, 10, 3, true}, + {"123456789", 123456789, 123456789, 9, true}, + {"123456789", 123456788, 0, 0, false}, + {"123456789123456789123456789", 999999999, 0, 0, false}, + } { + num, n, ok := parseToken(c.s, c.max) + if ok != c.ok { + t.Errorf("parseToken(%q, %d): got ok=%t want ok=%t", c.s, c.max, ok, c.ok) + continue + } + if !ok { + continue + } + if num != c.num || n != c.n { + t.Errorf("parseToken(%q, %d): got %d,%d,%t want %d,%d,%t", c.s, c.max, num, n, ok, c.num, c.n, c.ok) + } + } +} |
