diff --git a/internal/cli/cmds_test.go b/internal/cli/cmds_test.go index c085d024..4eb8884b 100644 --- a/internal/cli/cmds_test.go +++ b/internal/cli/cmds_test.go @@ -110,7 +110,7 @@ func TestFeatures(t *testing.T) { s.Before(func(ctx context.Context, sc *godog.Scenario) (context.Context, error) { ctx = context.WithValue(ctx, cmdAdditionalFlagsCtxKey{}, make(map[string]string)) ctx = context.WithValue(ctx, dockerResourcesCtxKey{}, []*dockertest.Resource{}) - ctx = context.WithValue(ctx, cmdStdInCtxKey{}, new(bytes.Buffer)) + ctx = context.WithValue(ctx, cmdStdInCtxKey{}, NewBlockBuffer()) return ctx, nil }) @@ -138,7 +138,7 @@ func TestFeatures(t *testing.T) { func wrapCmdOutputs(ctx context.Context) (context.Context, *cobra.Command) { rootCmd := cli.NewRootCmd() - cmdStdIn, isInteractive := ctx.Value(cmdStdInCtxKey{}).(*bytes.Buffer) + cmdStdIn, isInteractive := ctx.Value(cmdStdInCtxKey{}).(*BlockBuffer) if isInteractive { rootCmd.SetIn(cmdStdIn) } @@ -243,10 +243,11 @@ func aRecipesDirectory(ctx context.Context) (context.Context, error) { } func bufferKeysToInput(ctx context.Context, keys string) (context.Context, error) { - stdIn := ctx.Value(cmdStdInCtxKey{}).(*bytes.Buffer) + stdIn := ctx.Value(cmdStdInCtxKey{}).(*BlockBuffer) keys = strings.Replace(keys, "\\r", "\r", -1) - stdIn.WriteString(keys) + keys = strings.Replace(keys, "\\x1b", "\x1b", -1) + stdIn.Add([]byte(keys)) return context.WithValue(ctx, cmdStdInCtxKey{}, stdIn), nil } @@ -634,3 +635,51 @@ func nestedMapLookup(m map[string]interface{}, ks ...string) (rval interface{}, return nestedMapLookup(m, ks[1:]...) } } + +// BlockBuffer represents a buffer that stores blocks of data. +// This is used to simulate user input for the CLI. Using the standard buffer +// would not allow the simulation of complex key presses due to how bubbletea is implemented. +type BlockBuffer struct { + data [][]byte // The blocks of data stored in the buffer. + blockIndex int // The index of the current block. + readIndex int // The index of the next byte to be read. +} + +var _ io.Reader = &BlockBuffer{} + +func NewBlockBuffer() *BlockBuffer { + return &BlockBuffer{ + data: make([][]byte, 0), + blockIndex: 0, + readIndex: 0, + } +} + +func (r *BlockBuffer) Add(p []byte) { + r.data = append(r.data, p) +} + +func (r *BlockBuffer) Len() int { + s := 0 + for i := range r.data { + s += len(r.data[i]) + } + + return s +} + +func (r *BlockBuffer) Read(p []byte) (n int, err error) { + if r.blockIndex >= len(r.data) || r.readIndex >= len(r.data[r.blockIndex]) { + err = io.EOF + return + } + + n = copy(p, r.data[r.blockIndex][r.readIndex:]) + if n == len(r.data[r.blockIndex]) { + r.blockIndex++ + r.readIndex = 0 + } else { + r.readIndex += n + } + return +} diff --git a/internal/cli/execute_test.go b/internal/cli/execute_test.go index 48e6ae88..1610bedd 100644 --- a/internal/cli/execute_test.go +++ b/internal/cli/execute_test.go @@ -1,7 +1,6 @@ package cli_test import ( - "bytes" "context" "fmt" "os" @@ -20,7 +19,7 @@ func AddExecuteSteps(s *godog.ScenarioContext) { func iRunExecute(ctx context.Context, recipe string) (context.Context, error) { projectDir := ctx.Value(projectDirectoryPathCtxKey{}).(string) additionalFlags := ctx.Value(cmdAdditionalFlagsCtxKey{}).(map[string]string) - stdIn := ctx.Value(cmdStdInCtxKey{}).(*bytes.Buffer) + stdIn := ctx.Value(cmdStdInCtxKey{}).(*BlockBuffer) ctx, cmd := wrapCmdOutputs(ctx) diff --git a/internal/cli/upgrade_test.go b/internal/cli/upgrade_test.go index b142cd3c..83041e2a 100644 --- a/internal/cli/upgrade_test.go +++ b/internal/cli/upgrade_test.go @@ -26,7 +26,7 @@ func AddUpgradeSteps(s *godog.ScenarioContext) { func iRunUpgrade(ctx context.Context, recipe string) (context.Context, error) { projectDir := ctx.Value(projectDirectoryPathCtxKey{}).(string) additionalFlags := ctx.Value(cmdAdditionalFlagsCtxKey{}).(map[string]string) - stdIn := ctx.Value(cmdStdInCtxKey{}).(*bytes.Buffer) + stdIn := ctx.Value(cmdStdInCtxKey{}).(*BlockBuffer) ctx, cmd := wrapCmdOutputs(ctx) diff --git a/test/upgrade-recipe.feature b/test/upgrade-recipe.feature index caa1a298..bba56651 100644 --- a/test/upgrade-recipe.feature +++ b/test/upgrade-recipe.feature @@ -78,6 +78,20 @@ Feature: Upgrade sauce Then CLI produced an output "README.md: override" And the project directory should contain file "README.md" with "New version" + Scenario: Attempt upgrade when user overrides the locally modified file while using arrow keys + Given a project directory + And a recipes directory + And a recipe "foo" that generates file "README.md" + And I execute recipe "foo" + And I change recipe "foo" to version "v0.0.2" + And I change recipe "foo" template "README.md" to render "New version" + And I change project file "README.md" to contain "Locally modified" + And I buffer key presses "\x1b[C" + And I buffer key presses "\r" + When I upgrade recipe "foo" + Then CLI produced an output "README.md: override" + And the project directory should contain file "README.md" with "New version" + Scenario: Attempt upgrade when new file conflicts with existing manually created file Given a project directory And a recipes directory