pulumi/sdk/go/common/util/cmdutil/console_test.go

291 lines
6.9 KiB
Go
Raw Permalink Normal View History

// Copyright 2016-2021, Pulumi Corporation.
//
// 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 cmdutil
import (
"regexp"
"testing"
cmdutil.ReadConsole[NoEcho]: Use bubbletea (#13815) Switch the cmdutil.ReadConsole and cmdutil.ReadConsoleNoEcho functions to use the bubbletea library to render the prompt, using the textinput widget provided by the accompanying bubbles library. The resulting input widgets support arrow keys, back space, and some basic readline-style bindings including Ctrl-A, Alt-B, etc. I went through all uses of ReadConsole or ReadConsoleNoEcho. Only the one in new.go had a non-compliant prompt that I had to adjust. Note: One divergence in behavior I opted for was that password prompts will echo '*' characters as the user is typing and then no echo once they've accepted or canceled the value. Previously, the prompt did not echo anything in either case. <details> <summary> Introduction if you're unfamiliar with bubbletea </summary> bubbletea operates by modeling the widget state as an immutable data structure that receives messages for events. On receiving a message (key press, e.g.) the model's Update method returns a new model instance representing its new state. Update may also optionally return additional commands for the program, e.g. stop running, or print something and move on. The model's View method returns what should be drawn in the terminal based on the model's current state. This programming model makes it reasonably straightforward to unit test some of the core functionality of independent widgets as demonstrated in this PR. </details> Resolves #1565 --- Demos: <details> <summary>Plain text</summary> ![prompt-plain](https://github.com/pulumi/pulumi/assets/41730/66258fc8-f772-4d01-bc7c-1f7b116aebaa) </details> <details> <summary>Secret</summary> ![prompt-secret](https://github.com/pulumi/pulumi/assets/41730/372f862e-9186-4d47-ba7d-0107c47f52f6) </details> <details> <summary>Secret prompt with padding</summary> ![prompt-secret-2](https://github.com/pulumi/pulumi/assets/41730/e9b7c253-4c9d-4235-9fa6-197aa0522033) </details>
2023-08-30 17:08:44 +00:00
tea "github.com/charmbracelet/bubbletea"
"github.com/pulumi/pulumi/sdk/v3/go/common/diag/colors"
"github.com/stretchr/testify/assert"
)
func TestMeasureText(t *testing.T) {
t.Parallel()
cases := []struct {
text string
expected int
}{
{
text: "",
expected: 0,
},
{
text: "a",
expected: 1,
},
{
text: "├",
expected: 1,
},
{
text: "├─ ",
expected: 4,
},
{
text: "\x1b[4m\x1b[38;5;12mType\x1b[0m",
expected: 4,
},
}
for _, c := range cases {
c := c
t.Run(c.text, func(t *testing.T) {
t.Parallel()
count := MeasureText(c.text)
assert.Equal(t, c.expected, count)
})
}
}
func TestTablePrinting(t *testing.T) {
t.Parallel()
rows := []TableRow{
{Columns: []string{"A", "B", "C"}},
{Columns: []string{"Some A", "B", "Some C"}},
}
table := &Table{
Headers: []string{"ColumnA", "Long column B", "C"},
Rows: rows,
Prefix: " ",
}
expected := "" +
" ColumnA Long column B C\n" +
" A B C\n" +
" Some A B Some C\n"
assert.Equal(t, expected, table.ToStringWithGap(" "))
}
func TestColorTablePrinting(t *testing.T) {
t.Parallel()
greenText := func(msg string) string {
return colors.Always.Colorize(colors.Green + msg + colors.Reset)
}
rows := []TableRow{
{Columns: []string{greenText("+"), "pulumi:pulumi:Stack", "aws-cs-webserver-test", greenText("create")}},
{Columns: []string{greenText("+"), "├─ aws:ec2/instance:Instance", "web-server-www", greenText("create")}},
{Columns: []string{greenText("+"), "├─ aws:ec2/securityGroup:SecurityGroup", "web-secgrp", greenText("create")}},
{Columns: []string{greenText("+"), "└─ pulumi:providers:aws", "default_4_25_0", greenText("create")}},
}
columnHeader := func(msg string) string {
return colors.Always.Colorize(colors.Underline + colors.BrightBlue + msg + colors.Reset)
}
table := &Table{
Headers: []string{"", columnHeader("Type"), columnHeader("Name"), columnHeader("Plan")},
Rows: rows,
Prefix: " ",
}
expected := "" +
" Type Name Plan\n" +
" + pulumi:pulumi:Stack aws-cs-webserver-test create\n" +
" + ├─ aws:ec2/instance:Instance web-server-www create\n" +
" + ├─ aws:ec2/securityGroup:SecurityGroup web-secgrp create\n" +
" + └─ pulumi:providers:aws default_4_25_0 create\n"
colorTable := table.ToStringWithGap(" ")
// 7-bit C1 ANSI sequences
ansiEscape := regexp.MustCompile(`\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])`)
cleanTable := ansiEscape.ReplaceAllString(colorTable, "")
assert.Equal(t, expected, cleanTable)
}
func TestIsTruthy(t *testing.T) {
t.Parallel()
tests := []struct {
give string
want bool
}{
{"1", true},
{"true", true},
{"True", true},
{"TRUE", true},
{"0", false},
{"false", false},
{"False", false},
{"FALSE", false},
{"", false},
}
for _, tt := range tests {
tt := tt
t.Run(tt.give, func(t *testing.T) {
t.Parallel()
assert.Equal(t, tt.want, IsTruthy(tt.give))
})
}
}
cmdutil.ReadConsole[NoEcho]: Use bubbletea (#13815) Switch the cmdutil.ReadConsole and cmdutil.ReadConsoleNoEcho functions to use the bubbletea library to render the prompt, using the textinput widget provided by the accompanying bubbles library. The resulting input widgets support arrow keys, back space, and some basic readline-style bindings including Ctrl-A, Alt-B, etc. I went through all uses of ReadConsole or ReadConsoleNoEcho. Only the one in new.go had a non-compliant prompt that I had to adjust. Note: One divergence in behavior I opted for was that password prompts will echo '*' characters as the user is typing and then no echo once they've accepted or canceled the value. Previously, the prompt did not echo anything in either case. <details> <summary> Introduction if you're unfamiliar with bubbletea </summary> bubbletea operates by modeling the widget state as an immutable data structure that receives messages for events. On receiving a message (key press, e.g.) the model's Update method returns a new model instance representing its new state. Update may also optionally return additional commands for the program, e.g. stop running, or print something and move on. The model's View method returns what should be drawn in the terminal based on the model's current state. This programming model makes it reasonably straightforward to unit test some of the core functionality of independent widgets as demonstrated in this PR. </details> Resolves #1565 --- Demos: <details> <summary>Plain text</summary> ![prompt-plain](https://github.com/pulumi/pulumi/assets/41730/66258fc8-f772-4d01-bc7c-1f7b116aebaa) </details> <details> <summary>Secret</summary> ![prompt-secret](https://github.com/pulumi/pulumi/assets/41730/372f862e-9186-4d47-ba7d-0107c47f52f6) </details> <details> <summary>Secret prompt with padding</summary> ![prompt-secret-2](https://github.com/pulumi/pulumi/assets/41730/e9b7c253-4c9d-4235-9fa6-197aa0522033) </details>
2023-08-30 17:08:44 +00:00
func TestReadConsoleFancy(t *testing.T) {
t.Parallel()
tests := []struct {
desc string
model readConsoleModel
// Prompt expected on the command line, if any
// after which we begin typing.
expectPrompt string
// Messages to send to the model in order.
// Does not include Enter or Ctrl+C.
giveMsgs []tea.Msg
// Output visible before we hit Enter.
wantEcho string
// Output visible after we hit Enter.
wantAccepted string
// Value returned by the model.
wantValue string
}{
{
desc: "plain",
model: newReadConsoleModel("Enter a value", false /* secret */),
expectPrompt: "Enter a value: ",
giveMsgs: []tea.Msg{
tea.KeyMsg{
Type: tea.KeyRunes,
Runes: []rune("hello"),
},
},
wantEcho: "Enter a value: hello",
wantAccepted: "Enter a value: hello ",
wantValue: "hello",
},
{
desc: "secret",
model: newReadConsoleModel("Password", true /* secret */),
expectPrompt: "Password: ",
giveMsgs: []tea.Msg{
tea.KeyMsg{
Type: tea.KeyRunes,
Runes: []rune("hunter2"),
},
},
wantEcho: "Password: *******",
wantAccepted: "Password: ",
wantValue: "hunter2",
},
{
desc: "no prompt",
model: newReadConsoleModel("" /* prompt */, false /* secret */),
giveMsgs: []tea.Msg{
tea.KeyMsg{
Type: tea.KeyRunes,
Runes: []rune("hello"),
},
},
wantEcho: "> hello",
wantAccepted: "> hello ",
wantValue: "hello",
},
{
desc: "backspace",
model: newReadConsoleModel("" /* prompt */, false /* secret */),
giveMsgs: []tea.Msg{
tea.KeyMsg{
Type: tea.KeyRunes,
Runes: []rune("foobar"),
},
tea.KeyMsg{Type: tea.KeyBackspace},
tea.KeyMsg{Type: tea.KeyBackspace},
tea.KeyMsg{
Type: tea.KeyRunes,
Runes: []rune("az"),
},
},
wantEcho: "> foobaz",
wantAccepted: "> foobaz ",
wantValue: "foobaz",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.desc, func(t *testing.T) {
t.Parallel()
var m tea.Model = tt.model
m.Init()
if tt.expectPrompt != "" {
assert.Contains(t, m.View(), tt.expectPrompt,
"initial view should contain prompt")
}
for _, msg := range tt.giveMsgs {
m, _ = m.Update(msg)
}
assert.Contains(t, m.View(), tt.wantEcho,
"prompt before pressing enter did not match")
m, _ = m.Update(tea.KeyMsg{Type: tea.KeyEnter})
assert.Contains(t, m.View(), tt.wantAccepted,
"prompt after pressing enter did not match")
final, ok := m.(readConsoleModel)
assert.True(t, ok, "expected readConsoleModel, got %T", m)
assert.Equal(t, tt.wantValue, final.Value, "final value should match")
assert.False(t, final.Canceled, "should not be canceled")
})
}
}
func TestReadConsoleFancy_cancel(t *testing.T) {
t.Parallel()
var m tea.Model = newReadConsoleModel("Name:", false /* secret */)
m.Init()
m, _ = m.Update(tea.KeyMsg{
Type: tea.KeyRunes,
Runes: []rune("hello"),
})
m, _ = m.Update(tea.KeyMsg{Type: tea.KeyEsc})
final, ok := m.(readConsoleModel)
assert.True(t, ok, "expected readConsoleModel, got %T", m)
assert.True(t, final.Canceled, "should be canceled")
}