Skip to content

Commit

Permalink
Merge pull request #127 from scrapli/feat/with-interim-prompts
Browse files Browse the repository at this point in the history
feat: interim prompt patterns for sendX operations
  • Loading branch information
carlmontanari authored Apr 10, 2023
2 parents 3570aa3 + 9a86d89 commit 3b86569
Show file tree
Hide file tree
Showing 8 changed files with 167 additions and 13 deletions.
9 changes: 5 additions & 4 deletions channel/operation.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ const (
// strip the device prompt out, send eagerly (without reading until the input), and the timeout for
// the given operation.
type OperationOptions struct {
StripPrompt bool
Eager bool
Timeout time.Duration
CompletePatterns []*regexp.Regexp
StripPrompt bool
Eager bool
Timeout time.Duration
CompletePatterns []*regexp.Regexp
InterimPromptPatterns []*regexp.Regexp
}

// NewOperation returns a new OperationOptions object with the defaults set and any provided options
Expand Down
17 changes: 14 additions & 3 deletions channel/sendinput.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package channel

import (
"fmt"
"regexp"
"time"

"github.com/scrapli/scrapligo/util"
Expand Down Expand Up @@ -45,9 +46,19 @@ func (c *Channel) SendInputB(input []byte, opts ...util.Option) ([]byte, error)
if !op.Eager {
var nb []byte

nb, err = c.ReadUntilPrompt()
if err != nil {
cr <- &result{b: b, err: err}
var readErr error

if len(op.InterimPromptPatterns) == 0 {
nb, readErr = c.ReadUntilPrompt()
} else {
prompts := []*regexp.Regexp{c.PromptPattern}
prompts = append(prompts, op.InterimPromptPatterns...)

nb, readErr = c.ReadUntilAnyPrompt(prompts)
}

if readErr != nil {
cr <- &result{b: b, err: readErr}
}

b = append(b, nb...)
Expand Down
34 changes: 28 additions & 6 deletions driver/network/sendcommands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ package network_test
import (
"bytes"
"fmt"
"regexp"
"testing"

"github.com/scrapli/scrapligo/driver/opoptions"

"github.com/scrapli/scrapligo/util"

"github.com/scrapli/scrapligo/platform"
Expand All @@ -14,11 +17,12 @@ import (
)

type sendCommandsTestCase struct {
description string
commands []string
payloadFile string
stripPrompt bool
eager bool
description string
commands []string
payloadFile string
stripPrompt bool
eager bool
interimPrompts []*regexp.Regexp
}

func testSendCommands(testName string, testCase *sendCommandsTestCase) func(t *testing.T) {
Expand All @@ -27,7 +31,13 @@ func testSendCommands(testName string, testCase *sendCommandsTestCase) func(t *t

d, fileTransportObj := prepareDriver(t, testName, testCase.payloadFile)

r, err := d.SendCommands(testCase.commands)
var opts []util.Option

if len(testCase.interimPrompts) > 0 {
opts = append(opts, opoptions.WithInterimPromptPattern(testCase.interimPrompts))
}

r, err := d.SendCommands(testCase.commands, opts...)
if err != nil {
t.Fatalf(
"%s: encountered error running generic Driver SendCommands, error: %s",
Expand Down Expand Up @@ -80,6 +90,18 @@ func TestSendCommands(t *testing.T) {
stripPrompt: false,
eager: false,
},
"send-commands-interim-prompt": {
description: "simple send commands test with interim prompt patterns",
commands: []string{
"some command that starts interim prompt thing",
"subcommand1",
"subcommand2",
},
payloadFile: "send-commands-interim-prompt.txt",
stripPrompt: false,
eager: false,
interimPrompts: []*regexp.Regexp{regexp.MustCompile(`(?m)^\.{3}`)},
},
}

for testName, testCase := range cases {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@


some command that starts interim prompt thing


subcommand1


subcommand2

Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
...
...
5 changes: 5 additions & 0 deletions driver/network/test-fixtures/send-commands-interim-prompt.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
C3560CX#some command that starts interim prompt thing
... subcommand1
... subcommand2

C3560CX#
40 changes: 40 additions & 0 deletions driver/opoptions/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,43 @@ func WithCompletePatterns(p []*regexp.Regexp) util.Option {
return util.ErrIgnoredOption
}
}

// WithInterimPromptPattern is a slice of regex patterns that are valid prompts during a send X
// operation (either command or config as this is a channel level option). This can be used when
// devices change their prompt to indicate a multiline input.
// For example, when editing `trace-options` on nokia srl devices with scrapli(go) the prompt
// changes to ellipses to indicate you are editing the list still, this looks like this:
//
// ```
// A:srl# enter candidate private
// Candidate 'private-admin' is not empty
// --{ * candidate private private-admin }--[ ]--
// A:srl#
// --{ * candidate private private-admin }--[ ]--
// A:srl# system {
// --{ * candidate private private-admin }--[ system ]--
// A:srl# gnmi-server {
// --{ * candidate private private-admin }--[ system gnmi-server ]--
// A:srl# admin-state enable
// --{ * candidate private private-admin }--[ system gnmi-server ]--
// A:srl# trace-options [
// ...
// ````
//
// Without this option (or modifying the base comms prompt pattern/driver prompt patterns),
// scrapligo does not accept "..." as a prompt and will time out as it cant "find the prompt". This
// option allows you to cope with output like the above without modifying the driver/patterns
// themselves.
func WithInterimPromptPattern(p []*regexp.Regexp) util.Option {
return func(o interface{}) error {
c, ok := o.(*channel.OperationOptions)

if ok {
c.InterimPromptPatterns = p

return nil
}

return util.ErrIgnoredOption
}
}
63 changes: 63 additions & 0 deletions driver/opoptions/channel_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,3 +246,66 @@ func TestWithCompletePatterns(t *testing.T) {
t.Run(testName, f)
}
}

func testWithInterimPromptPatterns(testName string, testCase *struct {
description string
p []*regexp.Regexp
o interface{}
isignored bool
},
) func(t *testing.T) {
return func(t *testing.T) {
t.Logf("%s: starting", testName)

err := opoptions.WithInterimPromptPattern(testCase.p)(testCase.o)
if err != nil {
if errors.Is(err, util.ErrIgnoredOption) && !testCase.isignored {
t.Fatalf(
"%s: option should be ignored, but returned different error",
testName,
)
}

return
}

oo, _ := testCase.o.(*channel.OperationOptions)

if !cmp.Equal(oo.InterimPromptPatterns[0].String(), testCase.p[0].String()) {
t.Fatalf(
"%s: actual and expected interim patterns do not match\nactual: %v\nexpected:%v",
testName,
oo.InterimPromptPatterns[0].String(),
testCase.p[0].String(),
)
}
}
}

func TestWithInterimPromptPatterns(t *testing.T) {
cases := map[string]*struct {
description string
p []*regexp.Regexp
o interface{}
isignored bool
}{
"set-interim-patterns": {
description: "simple set option test",
p: []*regexp.Regexp{regexp.MustCompile(`pattern!`)},
o: &channel.OperationOptions{},
isignored: false,
},
"ignored": {
description: "skipped due to ignored type",
p: nil,
o: &network.Driver{},
isignored: true,
},
}

for testName, testCase := range cases {
f := testWithInterimPromptPatterns(testName, testCase)

t.Run(testName, f)
}
}

0 comments on commit 3b86569

Please sign in to comment.