Skip to content

Commit

Permalink
Add step over mode
Browse files Browse the repository at this point in the history
  • Loading branch information
inancgumus committed Dec 5, 2024
1 parent 8578d7a commit 78746fb
Show file tree
Hide file tree
Showing 7 changed files with 160 additions and 51 deletions.
58 changes: 49 additions & 9 deletions browser/breakpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

"github.com/gorilla/websocket"

"github.com/grafana/sobek"
"github.com/grafana/xk6-browser/env"
)

Expand Down Expand Up @@ -53,6 +54,12 @@ type breakpointRegistry struct {

muVariables sync.RWMutex
variables []map[string]debugVarFunc

// stepOverMode is used to pause the script execution
// on each line whether the breakpoint is hit or not.
//
// resume(true) disables the stepOverMode.
stepOverMode bool
}

func newBreakpointRegistry() *breakpointRegistry {
Expand All @@ -67,7 +74,8 @@ func newBreakpointRegistry() *breakpointRegistry {
// Line: 32,
// },
// },
pauser: make(chan chan struct{}, 1),
pauser: make(chan chan struct{}, 1),
stepOverMode: true,
}
}

Expand All @@ -83,10 +91,23 @@ func (br *breakpointRegistry) update(breakpoints []breakpoint) {
br.breakpoints = breakpoints
}

func (br *breakpointRegistry) shouldResume() bool {
return br.stepOverMode
}

func (br *breakpointRegistry) matches(p position) (breakpoint, bool) {
br.muBreakpoints.RLock()
defer br.muBreakpoints.RUnlock()

// stepOverMode allows pausing the script execution
// on the remaining lines.
if br.stepOverMode {
return breakpoint{
File: p.Filename,
Line: p.Line,
}, true
}

// We need to compare between /path/to/test-script.js and file:///path/to/test-script.js
for _, b := range br.breakpoints {
if strings.Contains(p.Filename, b.File) && b.Line == p.Line {
Expand All @@ -111,9 +132,22 @@ func (br *breakpointRegistry) pause(b breakpoint, column int, funcName string) e
}

// resume resumes the script execution.
func (br *breakpointRegistry) resume() {
c := <-br.pauser
close(c)
func (br *breakpointRegistry) resume(stepOut bool) {
log.Printf("resuming: stepOut=%v", stepOut)
if stepOut {
br.setStepOverMode(false)
}
select {
case c := <-br.pauser:
close(c)
default:
log.Printf("resuming: nothing to resume")
}
}

func (br *breakpointRegistry) setStepOverMode(on bool) {
log.Printf("stepOverMode set to %v", on)
br.stepOverMode = on
}

// setVar adds a variable to the registry.
Expand Down Expand Up @@ -149,19 +183,21 @@ func (br *breakpointRegistry) vars() []map[string]debugVarFunc {

// pauseOnBreakpoint is a helper that pauses the script execution
// when a breakpoint is hit in the script.
func pauseOnBreakpoint(vu moduleVU) {
bp := vu.breakpointRegistry
func pauseOnBreakpoint(bp *breakpointRegistry, rt *sobek.Runtime) {
if !bp.isActive() {
return
}

pos := getCurrentLineNumber(vu)
pos := getCurrentLineNumber(rt)
log.Printf("current line: %v", pos)

b, ok := bp.matches(pos)
if !ok {
return
}
if bp.shouldResume() {
bp.resume(false)
}

log.Printf("pausing at %v:%v", pos.Filename, pos.Line)
if err := bp.pause(b, pos.Column, pos.FuncName); err != nil {
Expand All @@ -172,7 +208,7 @@ func pauseOnBreakpoint(vu moduleVU) {
type breakpointUpdateResumer interface {
update(breakpoints []breakpoint)
vars() []map[string]debugVarFunc
resume()
resume(stepOut bool)
}

type breakpointClient struct {
Expand Down Expand Up @@ -251,10 +287,14 @@ func (bc *breakpointClient) updateBreakpoints(data []byte) {
}

func (bc *breakpointClient) handleResume() {
bc.registry.resume()
bc.registry.resume(true)
}

func (bc *breakpointClient) sendPause(b breakpoint, column int, funcName string) error {
if bc == nil {
return nil
}

type variable struct {
Name string `json:"name"`
Value any `json:"value"`
Expand Down
72 changes: 71 additions & 1 deletion browser/breakpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,11 @@ func (bpt *breakpointTest) update(breakpoints []breakpoint) {
bpt.updated = breakpoints
}

func (bpt *breakpointTest) resume() {
func (bpt *breakpointTest) resume(stepOut bool) {
bpt.mu.Lock()
defer bpt.mu.Unlock()
bpt.resumeCalled = true
_ = stepOut
}

func (bpt *breakpointTest) vars() []map[string]debugVarFunc {
Expand Down Expand Up @@ -143,3 +144,72 @@ func TestBreakpointClient_SendPause(t *testing.T) {
t.Fatalf("timeout waiting for server to handle the pause message")
}
}

func TestBreakpoint_Stepover(t *testing.T) {
t.Run("stepover", func(t *testing.T) {
reg := newBreakpointRegistry()
reg.setStepOverMode(true)
bp, ok := reg.matches(position{Filename: "foo.js", Line: 1})
assert.True(t, ok)
assert.Equal(t, breakpoint{File: "foo.js", Line: 1}, bp)
})

t.Run("stepover_off", func(t *testing.T) {
reg := newBreakpointRegistry()
reg.setStepOverMode(false)
_, ok := reg.matches(position{Filename: "foo.js", Line: 1})
assert.False(t, ok)
})

t.Run("stepover_off_with_breakpoint", func(t *testing.T) {
reg := newBreakpointRegistry()
reg.setStepOverMode(false)
reg.update([]breakpoint{{File: "foo.js", Line: 1}})
bp, ok := reg.matches(position{Filename: "foo.js", Line: 1})
assert.True(t, ok)
assert.Equal(t, breakpoint{File: "foo.js", Line: 1}, bp)
})

t.Run("pause_stepover_on", func(t *testing.T) {
reg := newBreakpointRegistry()
reg.setStepOverMode(true)

bp := breakpoint{File: "foo.js", Line: 1}

err := make(chan error)
go func() {
err <- reg.pause(bp, 0, "bar")
}()
go func() {
reg.resume(false)
}()
select {
case err := <-err:
require.NoError(t, err)
case <-time.After(5 * time.Second):
t.Fatalf("timeout waiting for pause to be called")
}
require.True(t, reg.stepOverMode)
})
t.Run("pause_stepover_off", func(t *testing.T) {
reg := newBreakpointRegistry()
reg.setStepOverMode(true)

bp := breakpoint{File: "foo.js", Line: 1}

err := make(chan error)
go func() {
err <- reg.pause(bp, 0, "bar")
}()
go func() {
reg.resume(true)
}()
select {
case err := <-err:
require.NoError(t, err)
case <-time.After(5 * time.Second):
t.Fatalf("timeout waiting for pause to be called")
}
require.False(t, reg.stepOverMode)
})
}
2 changes: 1 addition & 1 deletion browser/browser_context_mapping.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ func mapBrowserContext(vu moduleVU, bc *common.BrowserContext) mapping { //nolin
})
},
"cookies": func(urls ...string) *sobek.Promise {
pauseOnBreakpoint(vu)
pauseOnBreakpoint(vu.breakpointRegistry, vu.Runtime())

return k6ext.Promise(vu.Context(), func() (any, error) {
return bc.Cookies(urls...) //nolint:wrapcheck
Expand Down
4 changes: 2 additions & 2 deletions browser/browser_mapping.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func mapBrowser(vu moduleVU) mapping { //nolint:funlen,cyclop,gocognit
return b.IsConnected(), nil
},
"newContext": func(opts sobek.Value) (*sobek.Promise, error) {
pauseOnBreakpoint(vu)
pauseOnBreakpoint(vu.breakpointRegistry, vu.Runtime())

popts, err := parseBrowserContextOptions(vu.Runtime(), opts)
if err != nil {
Expand Down Expand Up @@ -73,7 +73,7 @@ func mapBrowser(vu moduleVU) mapping { //nolint:funlen,cyclop,gocognit
return b.Version(), nil
},
"newPage": func(opts sobek.Value) (*sobek.Promise, error) {
pauseOnBreakpoint(vu)
pauseOnBreakpoint(vu.breakpointRegistry, vu.Runtime())

popts, err := parseBrowserContextOptions(vu.Runtime(), opts)
if err != nil {
Expand Down
3 changes: 1 addition & 2 deletions browser/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,7 @@ func sobekEmptyString(v sobek.Value) bool {
return !sobekValueExists(v) || strings.TrimSpace(v.String()) == ""
}

func getCurrentLineNumber(vu moduleVU) position {
rt := vu.Runtime()
func getCurrentLineNumber(rt *sobek.Runtime) position {
// var parent string
var buf [2]sobek.StackFrame
frames := rt.CaptureCallStack(2, buf[:0])
Expand Down
Loading

0 comments on commit 78746fb

Please sign in to comment.