// 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" 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)) }) } } 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") }