test: refactor CLI create tests not to use PTY (#25807)

<!--

If you have used AI to produce some or all of this PR, please ensure you have read our [AI Contribution guidelines](https://coder.com/docs/about/contributing/AI_CONTRIBUTING) before submitting.

-->Part of https://github.com/coder/internal/issues/1400  
  
Refactors CLI tests of the `create` command as the first batch of tests refactored to take a PTY out of the loop.

One interesting difference I noticed between PTY and a direct pipe to standard in is that on the PTY we write `\r` to enter some input, but the kernel actually sends `\n` (or maybe `\r\n`) to the process, at least on Unix. (On windows we sent `\r\n` into the PTY).  This is reflected in the implementation of the `Writer` , otherwise mostly inspired by the PTYTest equivalents.
This commit is contained in:
Spike Curtis
2026-05-28 17:50:37 -04:00
committed by GitHub
parent a16de96611
commit ee4126e913
4 changed files with 262 additions and 192 deletions
+184 -175
View File
@@ -20,8 +20,8 @@ import (
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/provisioner/echo"
"github.com/coder/coder/v2/provisionersdk/proto"
"github.com/coder/coder/v2/pty/ptytest"
"github.com/coder/coder/v2/testutil"
"github.com/coder/coder/v2/testutil/expecter"
)
func TestCreateDynamic(t *testing.T) {
@@ -74,14 +74,14 @@ func TestCreateDynamic(t *testing.T) {
}
inv, root := clitest.New(t, args...)
clitest.SetupConfig(t, member, root)
pty := ptytest.New(t).Attach(inv)
stdout := expecter.NewAttachedToInvocation(t, inv)
doneChan := make(chan error)
go func() {
doneChan <- inv.Run()
}()
pty.ExpectMatchContext(ctx, "has been created")
stdout.ExpectMatchContext(ctx, "has been created")
err := testutil.RequireReceive(ctx, t, doneChan)
require.NoError(t, err)
@@ -103,14 +103,14 @@ func TestCreateDynamic(t *testing.T) {
}
inv, root = clitest.New(t, args...)
clitest.SetupConfig(t, member, root)
pty = ptytest.New(t).Attach(inv)
stdout = expecter.NewAttachedToInvocation(t, inv)
doneChan = make(chan error)
go func() {
doneChan <- inv.Run()
}()
pty.ExpectMatchContext(ctx, "has been created")
stdout.ExpectMatchContext(ctx, "has been created")
err = testutil.RequireReceive(ctx, t, doneChan)
require.NoError(t, err)
@@ -129,7 +129,8 @@ func TestCreateDynamic(t *testing.T) {
// When enable_region=true, the region parameter becomes required and CLI should prompt.
t.Run("PromptForConditionalParam", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
ctx := testutil.Context(t, time.Hour)
logger := testutil.Logger(t)
template, _ := coderdtest.DynamicParameterTemplate(t, owner, first.OrganizationID, coderdtest.DynamicParameterTemplateParams{
MainTF: conditionalParamTF,
@@ -143,7 +144,8 @@ func TestCreateDynamic(t *testing.T) {
}
inv, root := clitest.New(t, args...)
clitest.SetupConfig(t, member, root)
pty := ptytest.New(t).Attach(inv)
stdout := expecter.NewAttachedToInvocation(t, inv)
stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), inv)
doneChan := make(chan error)
go func() {
@@ -151,14 +153,14 @@ func TestCreateDynamic(t *testing.T) {
}()
// CLI should prompt for the region parameter since enable_region=true
pty.ExpectMatchContext(ctx, "region")
pty.WriteLine("eu-west")
stdout.ExpectMatchContext(ctx, "region")
stdin.WriteLine("eu-west")
// Confirm creation
pty.ExpectMatchContext(ctx, "Confirm create?")
pty.WriteLine("yes")
stdout.ExpectMatchContext(ctx, "Confirm create?")
stdin.WriteLine("yes")
pty.ExpectMatchContext(ctx, "has been created")
stdout.ExpectMatchContext(ctx, "has been created")
err := <-doneChan
require.NoError(t, err)
@@ -305,14 +307,14 @@ func TestCreateDynamic(t *testing.T) {
"-y",
)
clitest.SetupConfig(t, member, root)
pty := ptytest.New(t).Attach(inv)
stdout := expecter.NewAttachedToInvocation(t, inv)
doneChan := make(chan error)
go func() {
doneChan <- inv.Run()
}()
pty.ExpectMatchContext(ctx, "has been created")
stdout.ExpectMatchContext(ctx, "has been created")
err = <-doneChan
require.NoError(t, err, "slider=8 should succeed when max_slider=10")
@@ -331,6 +333,8 @@ func TestCreate(t *testing.T) {
t.Parallel()
t.Run("Create", func(t *testing.T) {
t.Parallel()
logger := testutil.Logger(t)
ctx := testutil.Context(t, testutil.WaitMedium)
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
owner := coderdtest.CreateFirstUser(t, client)
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
@@ -348,7 +352,8 @@ func TestCreate(t *testing.T) {
inv, root := clitest.New(t, args...)
clitest.SetupConfig(t, member, root)
doneChan := make(chan struct{})
pty := ptytest.New(t).Attach(inv)
stdout := expecter.NewAttachedToInvocation(t, inv)
stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), inv)
go func() {
defer close(doneChan)
err := inv.Run()
@@ -363,9 +368,9 @@ func TestCreate(t *testing.T) {
{match: "Confirm create", write: "yes"},
}
for _, m := range matches {
pty.ExpectMatch(m.match)
stdout.ExpectMatchContext(ctx, m.match)
if len(m.write) > 0 {
pty.WriteLine(m.write)
stdin.WriteLine(m.write)
}
}
<-doneChan
@@ -385,6 +390,8 @@ func TestCreate(t *testing.T) {
t.Run("CreateForOtherUser", func(t *testing.T) {
t.Parallel()
logger := testutil.Logger(t)
ctx := testutil.Context(t, testutil.WaitMedium)
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
owner := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, completeWithAgent())
@@ -403,7 +410,8 @@ func TestCreate(t *testing.T) {
//nolint:gocritic // Creating a workspace for another user requires owner permissions.
clitest.SetupConfig(t, client, root)
doneChan := make(chan struct{})
pty := ptytest.New(t).Attach(inv)
stdout := expecter.NewAttachedToInvocation(t, inv)
stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), inv)
go func() {
defer close(doneChan)
err := inv.Run()
@@ -418,9 +426,9 @@ func TestCreate(t *testing.T) {
{match: "Confirm create", write: "yes"},
}
for _, m := range matches {
pty.ExpectMatch(m.match)
stdout.ExpectMatchContext(ctx, m.match)
if len(m.write) > 0 {
pty.WriteLine(m.write)
stdin.WriteLine(m.write)
}
}
<-doneChan
@@ -439,6 +447,8 @@ func TestCreate(t *testing.T) {
t.Run("CreateWithSpecificTemplateVersion", func(t *testing.T) {
t.Parallel()
logger := testutil.Logger(t)
ctx := testutil.Context(t, testutil.WaitMedium)
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
owner := coderdtest.CreateFirstUser(t, client)
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
@@ -467,7 +477,8 @@ func TestCreate(t *testing.T) {
inv, root := clitest.New(t, args...)
clitest.SetupConfig(t, member, root)
doneChan := make(chan struct{})
pty := ptytest.New(t).Attach(inv)
stdout := expecter.NewAttachedToInvocation(t, inv)
stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), inv)
go func() {
defer close(doneChan)
err := inv.Run()
@@ -482,9 +493,9 @@ func TestCreate(t *testing.T) {
{match: "Confirm create", write: "yes"},
}
for _, m := range matches {
pty.ExpectMatch(m.match)
stdout.ExpectMatchContext(ctx, m.match)
if len(m.write) > 0 {
pty.WriteLine(m.write)
stdin.WriteLine(m.write)
}
}
<-doneChan
@@ -506,6 +517,8 @@ func TestCreate(t *testing.T) {
t.Run("InheritStopAfterFromTemplate", func(t *testing.T) {
t.Parallel()
logger := testutil.Logger(t)
ctx := testutil.Context(t, testutil.WaitMedium)
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
owner := coderdtest.CreateFirstUser(t, client)
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
@@ -522,7 +535,8 @@ func TestCreate(t *testing.T) {
}
inv, root := clitest.New(t, args...)
clitest.SetupConfig(t, member, root)
pty := ptytest.New(t).Attach(inv)
stdout := expecter.NewAttachedToInvocation(t, inv)
stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), inv)
waiter := clitest.StartWithWaiter(t, inv)
matches := []struct {
match string
@@ -533,9 +547,9 @@ func TestCreate(t *testing.T) {
{match: "Confirm create", write: "yes"},
}
for _, m := range matches {
pty.ExpectMatch(m.match)
stdout.ExpectMatchContext(ctx, m.match)
if len(m.write) > 0 {
pty.WriteLine(m.write)
stdin.WriteLine(m.write)
}
}
waiter.RequireSuccess()
@@ -570,6 +584,8 @@ func TestCreate(t *testing.T) {
t.Run("FromNothing", func(t *testing.T) {
t.Parallel()
logger := testutil.Logger(t)
ctx := testutil.Context(t, testutil.WaitMedium)
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
owner := coderdtest.CreateFirstUser(t, client)
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
@@ -579,7 +595,8 @@ func TestCreate(t *testing.T) {
inv, root := clitest.New(t, "create", "")
clitest.SetupConfig(t, member, root)
doneChan := make(chan struct{})
pty := ptytest.New(t).Attach(inv)
stdout := expecter.NewAttachedToInvocation(t, inv)
stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), inv)
go func() {
defer close(doneChan)
err := inv.Run()
@@ -592,8 +609,8 @@ func TestCreate(t *testing.T) {
for i := 0; i < len(matches); i += 2 {
match := matches[i]
value := matches[i+1]
pty.ExpectMatch(match)
pty.WriteLine(value)
stdout.ExpectMatchContext(ctx, match)
stdin.WriteLine(value)
}
<-doneChan
@@ -621,14 +638,14 @@ func TestCreate(t *testing.T) {
)
clitest.SetupConfig(t, member, root)
doneChan := make(chan struct{})
pty := ptytest.New(t).Attach(inv)
stdout := expecter.NewAttachedToInvocation(t, inv)
go func() {
defer close(doneChan)
err := inv.Run()
assert.NoError(t, err)
}()
pty.ExpectMatchContext(ctx, "building in the background")
stdout.ExpectMatchContext(ctx, "building in the background")
_ = testutil.TryReceive(ctx, t, doneChan)
// Verify workspace was actually created.
@@ -658,14 +675,14 @@ func TestCreate(t *testing.T) {
)
clitest.SetupConfig(t, member, root)
doneChan := make(chan struct{})
pty := ptytest.New(t).Attach(inv)
stdout := expecter.NewAttachedToInvocation(t, inv)
go func() {
defer close(doneChan)
err := inv.Run()
assert.NoError(t, err)
}()
pty.ExpectMatchContext(ctx, "building in the background")
stdout.ExpectMatchContext(ctx, "building in the background")
_ = testutil.TryReceive(ctx, t, doneChan)
// Verify workspace was created and parameters were applied.
@@ -706,14 +723,14 @@ func TestCreate(t *testing.T) {
)
clitest.SetupConfig(t, member, root)
doneChan := make(chan struct{})
pty := ptytest.New(t).Attach(inv)
stdout := expecter.NewAttachedToInvocation(t, inv)
go func() {
defer close(doneChan)
err := inv.Run()
assert.NoError(t, err)
}()
pty.ExpectMatchContext(ctx, "building in the background")
stdout.ExpectMatchContext(ctx, "building in the background")
_ = testutil.TryReceive(ctx, t, doneChan)
ws, err := member.WorkspaceByOwnerAndName(ctx, codersdk.Me, "my-workspace", codersdk.WorkspaceOptions{})
@@ -801,7 +818,7 @@ func TestCreateWithRichParameters(t *testing.T) {
setup func() []string
// handlePty optionally runs after the command is started. It should handle
// all expected prompts from the pty.
handlePty func(pty *ptytest.PTY)
handlePty func(ctx context.Context, stdout *expecter.Expecter, stdin *testutil.Writer)
// postRun runs after the command has finished but before the workspace is
// verified. It must return the workspace name to check (used for the copy
// workspace tests).
@@ -818,15 +835,15 @@ func TestCreateWithRichParameters(t *testing.T) {
}{
{
name: "ValuesFromPrompt",
handlePty: func(pty *ptytest.PTY) {
handlePty: func(ctx context.Context, stdout *expecter.Expecter, stdin *testutil.Writer) {
// Enter the value for each parameter as prompted.
for _, param := range params {
pty.ExpectMatch(param.name)
pty.WriteLine(param.value)
stdout.ExpectMatchContext(ctx, param.name)
stdin.WriteLine(param.value)
}
// Confirm the creation.
pty.ExpectMatch("Confirm create?")
pty.WriteLine("yes")
stdout.ExpectMatchContext(ctx, "Confirm create?")
stdin.WriteLine("yes")
},
},
{
@@ -839,16 +856,16 @@ func TestCreateWithRichParameters(t *testing.T) {
}
return args
},
handlePty: func(pty *ptytest.PTY) {
handlePty: func(ctx context.Context, stdout *expecter.Expecter, stdin *testutil.Writer) {
// Simply accept the defaults.
for _, param := range params {
pty.ExpectMatch(param.name)
pty.ExpectMatch(`Enter a value (default: "` + param.value + `")`)
pty.WriteLine("")
stdout.ExpectMatchContext(ctx, param.name)
stdout.ExpectMatchContext(ctx, `Enter a value (default: "`+param.value+`")`)
stdin.WriteLine("")
}
// Confirm the creation.
pty.ExpectMatch("Confirm create?")
pty.WriteLine("yes")
stdout.ExpectMatchContext(ctx, "Confirm create?")
stdin.WriteLine("yes")
},
},
{
@@ -865,10 +882,10 @@ func TestCreateWithRichParameters(t *testing.T) {
return []string{"--rich-parameter-file", parameterFile.Name()}
},
handlePty: func(pty *ptytest.PTY) {
handlePty: func(ctx context.Context, stdout *expecter.Expecter, stdin *testutil.Writer) {
// No prompts, we only need to confirm.
pty.ExpectMatch("Confirm create?")
pty.WriteLine("yes")
stdout.ExpectMatchContext(ctx, "Confirm create?")
stdin.WriteLine("yes")
},
},
{
@@ -881,10 +898,10 @@ func TestCreateWithRichParameters(t *testing.T) {
}
return args
},
handlePty: func(pty *ptytest.PTY) {
handlePty: func(ctx context.Context, stdout *expecter.Expecter, stdin *testutil.Writer) {
// No prompts, we only need to confirm.
pty.ExpectMatch("Confirm create?")
pty.WriteLine("yes")
stdout.ExpectMatchContext(ctx, "Confirm create?")
stdin.WriteLine("yes")
},
},
{
@@ -920,9 +937,6 @@ func TestCreateWithRichParameters(t *testing.T) {
postRun: func(t *testing.T, tctx testContext) string {
inv, root := clitest.New(t, "create", "--copy-parameters-from", tctx.workspaceName, "other-workspace", "-y")
clitest.SetupConfig(t, tctx.member, root)
pty := ptytest.New(t).Attach(inv)
inv.Stdout = pty.Output()
inv.Stderr = pty.Output()
err := inv.Run()
require.NoError(t, err, "failed to create a workspace based on the source workspace")
return "other-workspace"
@@ -952,9 +966,6 @@ func TestCreateWithRichParameters(t *testing.T) {
// Then create the copy. It should use the old template version.
inv, root := clitest.New(t, "create", "--copy-parameters-from", tctx.workspaceName, "other-workspace", "-y")
clitest.SetupConfig(t, tctx.member, root)
pty := ptytest.New(t).Attach(inv)
inv.Stdout = pty.Output()
inv.Stderr = pty.Output()
err := inv.Run()
require.NoError(t, err, "failed to create a workspace based on the source workspace")
return "other-workspace"
@@ -962,16 +973,16 @@ func TestCreateWithRichParameters(t *testing.T) {
},
{
name: "ValuesFromTemplateDefaults",
handlePty: func(pty *ptytest.PTY) {
handlePty: func(ctx context.Context, stdout *expecter.Expecter, stdin *testutil.Writer) {
// Simply accept the defaults.
for _, param := range params {
pty.ExpectMatch(param.name)
pty.ExpectMatch(`Enter a value (default: "` + param.value + `")`)
pty.WriteLine("")
stdout.ExpectMatchContext(ctx, param.name)
stdout.ExpectMatchContext(ctx, `Enter a value (default: "`+param.value+`")`)
stdin.WriteLine("")
}
// Confirm the creation.
pty.ExpectMatch("Confirm create?")
pty.WriteLine("yes")
stdout.ExpectMatchContext(ctx, "Confirm create?")
stdin.WriteLine("yes")
},
withDefaults: true,
},
@@ -980,14 +991,14 @@ func TestCreateWithRichParameters(t *testing.T) {
setup: func() []string {
return []string{"--use-parameter-defaults"}
},
handlePty: func(pty *ptytest.PTY) {
handlePty: func(ctx context.Context, stdout *expecter.Expecter, stdin *testutil.Writer) {
// Default values should get printed.
for _, param := range params {
pty.ExpectMatch(fmt.Sprintf("%s: '%s'", param.name, param.value))
stdout.ExpectMatchContext(ctx, fmt.Sprintf("%s: '%s'", param.name, param.value))
}
// No prompts, we only need to confirm.
pty.ExpectMatch("Confirm create?")
pty.WriteLine("yes")
stdout.ExpectMatchContext(ctx, "Confirm create?")
stdin.WriteLine("yes")
},
withDefaults: true,
},
@@ -1001,14 +1012,14 @@ func TestCreateWithRichParameters(t *testing.T) {
}
return args
},
handlePty: func(pty *ptytest.PTY) {
handlePty: func(ctx context.Context, stdout *expecter.Expecter, stdin *testutil.Writer) {
// Default values should get printed.
for _, param := range params {
pty.ExpectMatch(fmt.Sprintf("%s: '%s'", param.name, param.value))
stdout.ExpectMatchContext(ctx, fmt.Sprintf("%s: '%s'", param.name, param.value))
}
// No prompts, we only need to confirm.
pty.ExpectMatch("Confirm create?")
pty.WriteLine("yes")
stdout.ExpectMatchContext(ctx, "Confirm create?")
stdin.WriteLine("yes")
},
},
{
@@ -1031,14 +1042,14 @@ cli_param: from file`)
"--parameter", "cli_param=from cli",
}
},
handlePty: func(pty *ptytest.PTY) {
handlePty: func(ctx context.Context, stdout *expecter.Expecter, stdin *testutil.Writer) {
// Should get prompted for the input param since it has no default.
pty.ExpectMatch("input_param")
pty.WriteLine("from input")
stdout.ExpectMatchContext(ctx, "input_param")
stdin.WriteLine("from input")
// Confirm the creation.
pty.ExpectMatch("Confirm create?")
pty.WriteLine("yes")
stdout.ExpectMatchContext(ctx, "Confirm create?")
stdin.WriteLine("yes")
},
withDefaults: true,
inputParameters: []param{
@@ -1082,6 +1093,8 @@ cli_param: from file`)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
logger := testutil.Logger(t)
ctx := testutil.Context(t, testutil.WaitMedium)
parameters := params
if len(tt.inputParameters) > 0 {
@@ -1122,14 +1135,15 @@ cli_param: from file`)
inv, root := clitest.New(t, args...)
clitest.SetupConfig(t, member, root)
doneChan := make(chan error)
pty := ptytest.New(t).Attach(inv)
stdout := expecter.NewAttachedToInvocation(t, inv)
stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), inv)
go func() {
doneChan <- inv.Run()
}()
// The test may do something with the pty.
if tt.handlePty != nil {
tt.handlePty(pty)
tt.handlePty(ctx, stdout, stdin)
}
// Wait for the command to exit.
@@ -1235,6 +1249,7 @@ func TestCreateWithPreset(t *testing.T) {
// the CLI uses the specified preset instead of the default
t.Run("PresetFlag", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitMedium)
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
owner := coderdtest.CreateFirstUser(t, client)
@@ -1263,17 +1278,15 @@ func TestCreateWithPreset(t *testing.T) {
workspaceName := "my-workspace"
inv, root := clitest.New(t, "create", workspaceName, "--template", template.Name, "-y", "--preset", preset.Name)
clitest.SetupConfig(t, member, root)
pty := ptytest.New(t).Attach(inv)
inv.Stdout = pty.Output()
inv.Stderr = pty.Output()
stdout := expecter.NewAttachedToInvocation(t, inv)
err := inv.Run()
require.NoError(t, err)
// Should: display the selected preset as well as its parameters
presetName := fmt.Sprintf("Preset '%s' applied:", preset.Name)
pty.ExpectMatch(presetName)
pty.ExpectMatch(fmt.Sprintf("%s: '%s'", firstParameterName, secondOptionalParameterValue))
pty.ExpectMatch(fmt.Sprintf("%s: '%s'", thirdParameterName, thirdParameterValue))
stdout.ExpectMatchContext(ctx, presetName)
stdout.ExpectMatchContext(ctx, fmt.Sprintf("%s: '%s'", firstParameterName, secondOptionalParameterValue))
stdout.ExpectMatchContext(ctx, fmt.Sprintf("%s: '%s'", thirdParameterName, thirdParameterValue))
// Verify if the new workspace uses expected parameters.
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
@@ -1312,6 +1325,7 @@ func TestCreateWithPreset(t *testing.T) {
// the CLI automatically uses the default preset to create the workspace
t.Run("DefaultPreset", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitMedium)
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
owner := coderdtest.CreateFirstUser(t, client)
@@ -1340,22 +1354,17 @@ func TestCreateWithPreset(t *testing.T) {
workspaceName := "my-workspace"
inv, root := clitest.New(t, "create", workspaceName, "--template", template.Name, "-y")
clitest.SetupConfig(t, member, root)
pty := ptytest.New(t).Attach(inv)
inv.Stdout = pty.Output()
inv.Stderr = pty.Output()
stdout := expecter.NewAttachedToInvocation(t, inv)
err := inv.Run()
require.NoError(t, err)
// Should: display the default preset as well as its parameters
presetName := fmt.Sprintf("Preset '%s' (default) applied:", defaultPreset.Name)
pty.ExpectMatch(presetName)
pty.ExpectMatch(fmt.Sprintf("%s: '%s'", firstParameterName, secondOptionalParameterValue))
pty.ExpectMatch(fmt.Sprintf("%s: '%s'", thirdParameterName, thirdParameterValue))
stdout.ExpectMatchContext(ctx, presetName)
stdout.ExpectMatchContext(ctx, fmt.Sprintf("%s: '%s'", firstParameterName, secondOptionalParameterValue))
stdout.ExpectMatchContext(ctx, fmt.Sprintf("%s: '%s'", thirdParameterName, thirdParameterValue))
// Verify if the new workspace uses expected parameters.
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
defer cancel()
tvPresets, err := client.TemplateVersionPresets(ctx, version.ID)
require.NoError(t, err)
require.Len(t, tvPresets, 2)
@@ -1389,12 +1398,14 @@ func TestCreateWithPreset(t *testing.T) {
// the CLI prompts the user to select a preset.
t.Run("NoDefaultPresetPromptUser", func(t *testing.T) {
t.Parallel()
logger := testutil.Logger(t)
ctx := testutil.Context(t, testutil.WaitMedium)
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
owner := coderdtest.CreateFirstUser(t, client)
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
// Given: a template and a template version with two presets
// Given: a template and a template version with a single, non-default preset.
preset := proto.Preset{
Name: "preset-test",
Description: "Preset Test.",
@@ -1414,7 +1425,8 @@ func TestCreateWithPreset(t *testing.T) {
"--parameter", fmt.Sprintf("%s=%s", thirdParameterName, thirdParameterValue))
clitest.SetupConfig(t, member, root)
doneChan := make(chan struct{})
pty := ptytest.New(t).Attach(inv)
stdout := expecter.NewAttachedToInvocation(t, inv)
stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), inv)
go func() {
defer close(doneChan)
err := inv.Run()
@@ -1422,18 +1434,16 @@ func TestCreateWithPreset(t *testing.T) {
}()
// Should: prompt the user for the preset
pty.ExpectMatch("Select a preset below:")
pty.WriteLine("\n")
pty.ExpectMatch("Preset 'preset-test' applied")
pty.ExpectMatch("Confirm create?")
pty.WriteLine("yes")
stdout.ExpectMatchContext(ctx, "Select a preset below:")
// We don't actually have to respond to the selector, since we hardcode the cliui.Select to return the
// first option in test scenarios (c.f. cliui/select.go)
stdout.ExpectMatchContext(ctx, "Preset 'preset-test' applied")
stdout.ExpectMatchContext(ctx, "Confirm create?")
stdin.WriteLine("yes")
<-doneChan
// Verify if the new workspace uses expected parameters.
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
defer cancel()
tvPresets, err := client.TemplateVersionPresets(ctx, version.ID)
require.NoError(t, err)
require.Len(t, tvPresets, 1)
@@ -1460,6 +1470,7 @@ func TestCreateWithPreset(t *testing.T) {
// with workspace creation without applying any preset.
t.Run("TemplateVersionWithoutPresets", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitMedium)
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
owner := coderdtest.CreateFirstUser(t, client)
@@ -1476,17 +1487,12 @@ func TestCreateWithPreset(t *testing.T) {
"--parameter", fmt.Sprintf("%s=%s", firstParameterName, firstOptionalParameterValue),
"--parameter", fmt.Sprintf("%s=%s", thirdParameterName, thirdParameterValue))
clitest.SetupConfig(t, member, root)
pty := ptytest.New(t).Attach(inv)
inv.Stdout = pty.Output()
inv.Stderr = pty.Output()
stdout := expecter.NewAttachedToInvocation(t, inv)
err := inv.Run()
require.NoError(t, err)
pty.ExpectMatch("No preset applied.")
stdout.ExpectMatchContext(ctx, "No preset applied.")
// Verify if the new workspace uses expected parameters.
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
defer cancel()
workspaces, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
Name: workspaceName,
})
@@ -1509,6 +1515,7 @@ func TestCreateWithPreset(t *testing.T) {
// The workspace should be created without using any preset-defined parameters.
t.Run("PresetFlagNone", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitMedium)
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
owner := coderdtest.CreateFirstUser(t, client)
@@ -1533,17 +1540,12 @@ func TestCreateWithPreset(t *testing.T) {
"--parameter", fmt.Sprintf("%s=%s", firstParameterName, firstOptionalParameterValue),
"--parameter", fmt.Sprintf("%s=%s", thirdParameterName, thirdParameterValue))
clitest.SetupConfig(t, member, root)
pty := ptytest.New(t).Attach(inv)
inv.Stdout = pty.Output()
inv.Stderr = pty.Output()
stdout := expecter.NewAttachedToInvocation(t, inv)
err := inv.Run()
require.NoError(t, err)
pty.ExpectMatch("No preset applied.")
stdout.ExpectMatchContext(ctx, "No preset applied.")
// Verify that the new workspace doesn't use the preset parameters.
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
defer cancel()
tvPresets, err := client.TemplateVersionPresets(ctx, version.ID)
require.NoError(t, err)
require.Len(t, tvPresets, 1)
@@ -1591,9 +1593,6 @@ func TestCreateWithPreset(t *testing.T) {
workspaceName := "my-workspace"
inv, root := clitest.New(t, "create", workspaceName, "--template", template.Name, "-y", "--preset", "invalid-preset")
clitest.SetupConfig(t, member, root)
pty := ptytest.New(t).Attach(inv)
inv.Stdout = pty.Output()
inv.Stderr = pty.Output()
err := inv.Run()
// Should: fail with an error indicating the preset was not found
@@ -1610,6 +1609,7 @@ func TestCreateWithPreset(t *testing.T) {
// - and the value of parameter B from the parameter flag.
t.Run("PresetOverridesParameterFlagValues", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitMedium)
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
owner := coderdtest.CreateFirstUser(t, client)
@@ -1633,21 +1633,16 @@ func TestCreateWithPreset(t *testing.T) {
"--parameter", fmt.Sprintf("%s=%s", firstParameterName, firstOptionalParameterValue),
"--parameter", fmt.Sprintf("%s=%s", thirdParameterName, thirdParameterValue))
clitest.SetupConfig(t, member, root)
pty := ptytest.New(t).Attach(inv)
inv.Stdout = pty.Output()
inv.Stderr = pty.Output()
stdout := expecter.NewAttachedToInvocation(t, inv)
err := inv.Run()
require.NoError(t, err)
// Should: display the selected preset as well as its parameter
presetName := fmt.Sprintf("Preset '%s' applied:", preset.Name)
pty.ExpectMatch(presetName)
pty.ExpectMatch(fmt.Sprintf("%s: '%s'", firstParameterName, secondOptionalParameterValue))
stdout.ExpectMatchContext(ctx, presetName)
stdout.ExpectMatchContext(ctx, fmt.Sprintf("%s: '%s'", firstParameterName, secondOptionalParameterValue))
// Verify if the new workspace uses expected parameters.
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
defer cancel()
tvPresets, err := client.TemplateVersionPresets(ctx, version.ID)
require.NoError(t, err)
require.Len(t, tvPresets, 1)
@@ -1679,6 +1674,7 @@ func TestCreateWithPreset(t *testing.T) {
// - and the value of parameter B from the file.
t.Run("PresetOverridesParameterFileValues", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitMedium)
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
owner := coderdtest.CreateFirstUser(t, client)
@@ -1707,21 +1703,16 @@ func TestCreateWithPreset(t *testing.T) {
"--preset", preset.Name,
"--rich-parameter-file", parameterFile.Name())
clitest.SetupConfig(t, member, root)
pty := ptytest.New(t).Attach(inv)
inv.Stdout = pty.Output()
inv.Stderr = pty.Output()
stdout := expecter.NewAttachedToInvocation(t, inv)
err := inv.Run()
require.NoError(t, err)
// Should: display the selected preset as well as its parameter
presetName := fmt.Sprintf("Preset '%s' applied:", preset.Name)
pty.ExpectMatch(presetName)
pty.ExpectMatch(fmt.Sprintf("%s: '%s'", firstParameterName, secondOptionalParameterValue))
stdout.ExpectMatchContext(ctx, presetName)
stdout.ExpectMatchContext(ctx, fmt.Sprintf("%s: '%s'", firstParameterName, secondOptionalParameterValue))
// Verify if the new workspace uses expected parameters.
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
defer cancel()
tvPresets, err := client.TemplateVersionPresets(ctx, version.ID)
require.NoError(t, err)
require.Len(t, tvPresets, 1)
@@ -1748,7 +1739,8 @@ func TestCreateWithPreset(t *testing.T) {
// the CLI prompts the user for input to fill in the missing parameters.
t.Run("PromptsForMissingParametersWhenPresetIsIncomplete", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitMedium)
logger := testutil.Logger(t)
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
owner := coderdtest.CreateFirstUser(t, client)
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
@@ -1769,7 +1761,8 @@ func TestCreateWithPreset(t *testing.T) {
inv, root := clitest.New(t, "create", workspaceName, "--template", template.Name, "--preset", preset.Name)
clitest.SetupConfig(t, member, root)
doneChan := make(chan struct{})
pty := ptytest.New(t).Attach(inv)
stdout := expecter.NewAttachedToInvocation(t, inv)
stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), inv)
go func() {
defer close(doneChan)
err := inv.Run()
@@ -1778,21 +1771,18 @@ func TestCreateWithPreset(t *testing.T) {
// Should: display the selected preset as well as its parameters
presetName := fmt.Sprintf("Preset '%s' applied:", preset.Name)
pty.ExpectMatch(presetName)
pty.ExpectMatch(fmt.Sprintf("%s: '%s'", firstParameterName, secondOptionalParameterValue))
stdout.ExpectMatchContext(ctx, presetName)
stdout.ExpectMatchContext(ctx, fmt.Sprintf("%s: '%s'", firstParameterName, secondOptionalParameterValue))
// Should: prompt for the missing parameter
pty.ExpectMatch(thirdParameterDescription)
pty.WriteLine(thirdParameterValue)
pty.ExpectMatch("Confirm create?")
pty.WriteLine("yes")
stdout.ExpectMatchContext(ctx, thirdParameterDescription)
stdin.WriteLine(thirdParameterValue)
stdout.ExpectMatchContext(ctx, "Confirm create?")
stdin.WriteLine("yes")
<-doneChan
// Verify if the new workspace uses expected parameters.
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
tvPresets, err := client.TemplateVersionPresets(ctx, version.ID)
require.NoError(t, err)
require.Len(t, tvPresets, 1)
@@ -1857,7 +1847,8 @@ func TestCreateValidateRichParameters(t *testing.T) {
t.Run("ValidateString", func(t *testing.T) {
t.Parallel()
logger := testutil.Logger(t)
ctx := testutil.Context(t, testutil.WaitMedium)
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
owner := coderdtest.CreateFirstUser(t, client)
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
@@ -1869,7 +1860,8 @@ func TestCreateValidateRichParameters(t *testing.T) {
inv, root := clitest.New(t, "create", "my-workspace", "--template", template.Name)
clitest.SetupConfig(t, member, root)
doneChan := make(chan struct{})
pty := ptytest.New(t).Attach(inv)
stdout := expecter.NewAttachedToInvocation(t, inv)
stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), inv)
go func() {
defer close(doneChan)
err := inv.Run()
@@ -1885,9 +1877,9 @@ func TestCreateValidateRichParameters(t *testing.T) {
for i := 0; i < len(matches); i += 2 {
match := matches[i]
value := matches[i+1]
pty.ExpectMatch(match)
stdout.ExpectMatchContext(ctx, match)
if value != "" {
pty.WriteLine(value)
stdin.WriteLine(value)
}
}
<-doneChan
@@ -1895,6 +1887,8 @@ func TestCreateValidateRichParameters(t *testing.T) {
t.Run("ValidateNumber", func(t *testing.T) {
t.Parallel()
logger := testutil.Logger(t)
ctx := testutil.Context(t, testutil.WaitMedium)
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
owner := coderdtest.CreateFirstUser(t, client)
@@ -1907,7 +1901,8 @@ func TestCreateValidateRichParameters(t *testing.T) {
inv, root := clitest.New(t, "create", "my-workspace", "--template", template.Name)
clitest.SetupConfig(t, member, root)
doneChan := make(chan struct{})
pty := ptytest.New(t).Attach(inv)
stdout := expecter.NewAttachedToInvocation(t, inv)
stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), inv)
go func() {
defer close(doneChan)
err := inv.Run()
@@ -1923,9 +1918,9 @@ func TestCreateValidateRichParameters(t *testing.T) {
for i := 0; i < len(matches); i += 2 {
match := matches[i]
value := matches[i+1]
pty.ExpectMatch(match)
stdout.ExpectMatchContext(ctx, match)
if value != "" {
pty.WriteLine(value)
stdin.WriteLine(value)
}
}
<-doneChan
@@ -1933,6 +1928,8 @@ func TestCreateValidateRichParameters(t *testing.T) {
t.Run("ValidateNumber_CustomError", func(t *testing.T) {
t.Parallel()
logger := testutil.Logger(t)
ctx := testutil.Context(t, testutil.WaitMedium)
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
owner := coderdtest.CreateFirstUser(t, client)
@@ -1945,7 +1942,8 @@ func TestCreateValidateRichParameters(t *testing.T) {
inv, root := clitest.New(t, "create", "my-workspace", "--template", template.Name)
clitest.SetupConfig(t, member, root)
doneChan := make(chan struct{})
pty := ptytest.New(t).Attach(inv)
stdout := expecter.NewAttachedToInvocation(t, inv)
stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), inv)
go func() {
defer close(doneChan)
err := inv.Run()
@@ -1961,9 +1959,9 @@ func TestCreateValidateRichParameters(t *testing.T) {
for i := 0; i < len(matches); i += 2 {
match := matches[i]
value := matches[i+1]
pty.ExpectMatch(match)
stdout.ExpectMatchContext(ctx, match)
if value != "" {
pty.WriteLine(value)
stdin.WriteLine(value)
}
}
<-doneChan
@@ -1971,6 +1969,8 @@ func TestCreateValidateRichParameters(t *testing.T) {
t.Run("ValidateBool", func(t *testing.T) {
t.Parallel()
logger := testutil.Logger(t)
ctx := testutil.Context(t, testutil.WaitMedium)
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
owner := coderdtest.CreateFirstUser(t, client)
@@ -1983,7 +1983,8 @@ func TestCreateValidateRichParameters(t *testing.T) {
inv, root := clitest.New(t, "create", "my-workspace", "--template", template.Name)
clitest.SetupConfig(t, member, root)
doneChan := make(chan struct{})
pty := ptytest.New(t).Attach(inv)
stdout := expecter.NewAttachedToInvocation(t, inv)
stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), inv)
go func() {
defer close(doneChan)
err := inv.Run()
@@ -1999,9 +2000,9 @@ func TestCreateValidateRichParameters(t *testing.T) {
for i := 0; i < len(matches); i += 2 {
match := matches[i]
value := matches[i+1]
pty.ExpectMatch(match)
stdout.ExpectMatchContext(ctx, match)
if value != "" {
pty.WriteLine(value)
stdin.WriteLine(value)
}
}
<-doneChan
@@ -2018,15 +2019,18 @@ func TestCreateValidateRichParameters(t *testing.T) {
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
t.Run("Prompt", func(t *testing.T) {
logger := testutil.Logger(t)
ctx := testutil.Context(t, testutil.WaitMedium)
inv, root := clitest.New(t, "create", "my-workspace-1", "--template", template.Name)
clitest.SetupConfig(t, member, root)
pty := ptytest.New(t).Attach(inv)
stdout := expecter.NewAttachedToInvocation(t, inv)
stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), inv)
clitest.Start(t, inv)
pty.ExpectMatch(listOfStringsParameterName)
pty.ExpectMatch("aaa, bbb, ccc")
pty.ExpectMatch("Confirm create?")
pty.WriteLine("yes")
stdout.ExpectMatchContext(ctx, listOfStringsParameterName)
stdout.ExpectMatchContext(ctx, "aaa, bbb, ccc")
stdout.ExpectMatchContext(ctx, "Confirm create?")
stdin.WriteLine("yes")
})
t.Run("Default", func(t *testing.T) {
@@ -2049,6 +2053,8 @@ func TestCreateValidateRichParameters(t *testing.T) {
t.Run("ValidateListOfStrings_YAMLFile", func(t *testing.T) {
t.Parallel()
logger := testutil.Logger(t)
ctx := testutil.Context(t, testutil.WaitMedium)
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
owner := coderdtest.CreateFirstUser(t, client)
@@ -2066,8 +2072,8 @@ func TestCreateValidateRichParameters(t *testing.T) {
- fff`)
inv, root := clitest.New(t, "create", "my-workspace", "--template", template.Name, "--rich-parameter-file", parameterFile.Name())
clitest.SetupConfig(t, member, root)
pty := ptytest.New(t).Attach(inv)
stdout := expecter.NewAttachedToInvocation(t, inv)
stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), inv)
clitest.Start(t, inv)
matches := []string{
@@ -2076,9 +2082,9 @@ func TestCreateValidateRichParameters(t *testing.T) {
for i := 0; i < len(matches); i += 2 {
match := matches[i]
value := matches[i+1]
pty.ExpectMatch(match)
stdout.ExpectMatchContext(ctx, match)
if value != "" {
pty.WriteLine(value)
stdin.WriteLine(value)
}
}
})
@@ -2086,6 +2092,8 @@ func TestCreateValidateRichParameters(t *testing.T) {
func TestCreateWithGitAuth(t *testing.T) {
t.Parallel()
logger := testutil.Logger(t)
ctx := testutil.Context(t, testutil.WaitMedium)
echoResponses := &echo.Responses{
Parse: echo.ParseComplete,
ProvisionInit: echo.InitComplete,
@@ -2120,13 +2128,14 @@ func TestCreateWithGitAuth(t *testing.T) {
inv, root := clitest.New(t, "create", "my-workspace", "--template", template.Name)
clitest.SetupConfig(t, member, root)
pty := ptytest.New(t).Attach(inv)
stdout := expecter.NewAttachedToInvocation(t, inv)
stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), inv)
clitest.Start(t, inv)
pty.ExpectMatch("You must authenticate with GitHub to create a workspace")
stdout.ExpectMatchContext(ctx, "You must authenticate with GitHub to create a workspace")
resp := coderdtest.RequestExternalAuthCallback(t, "github", member)
_ = resp.Body.Close()
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
pty.ExpectMatch("Confirm create?")
pty.WriteLine("yes")
stdout.ExpectMatchContext(ctx, "Confirm create?")
stdin.WriteLine("yes")
}
+3 -14
View File
@@ -72,15 +72,10 @@ func (p *PTY) Close() error {
if pErr != nil {
p.Logf("PTY: Close failed: %v", pErr)
}
eErr := p.Expecter.Close("PTY close")
if eErr != nil {
p.Logf("PTY: close expecter failed: %v", eErr)
}
p.Expecter.Close("PTY close")
if pErr != nil {
p.closeErr = pErr
return
}
p.closeErr = eErr
})
return p.closeErr
}
@@ -135,12 +130,6 @@ func (p *PTYCmd) Close() error {
if pErr != nil {
p.Logf("PTYCmd: Close failed: %v", pErr)
}
eErr := p.Expecter.Close("PTYCmd close")
if eErr != nil {
p.Logf("PTYCmd: close expecter failed: %v", eErr)
}
if pErr != nil {
return pErr
}
return eErr
p.Expecter.Close("PTYCmd close")
return pErr
}
+20 -3
View File
@@ -19,6 +19,7 @@ import (
"golang.org/x/xerrors"
"github.com/coder/coder/v2/testutil"
"github.com/coder/serpent"
)
func New(t *testing.T, r io.Reader, name string) *Expecter {
@@ -68,6 +69,22 @@ func New(t *testing.T, r io.Reader, name string) *Expecter {
return ex
}
func NewAttachedToInvocation(t *testing.T, invocation *serpent.Invocation) *Expecter {
r, w := io.Pipe()
invocation.Stdout = w
invocation.Stderr = w
e := New(t, r, "cmd")
t.Cleanup(func() {
// Serpent doesn't handle closing stdout after running the Invocation; normally the OS does that automatically when
// the process exits. Close it here at the end of the test to ensure we don't leak goroutines reading from the
// stdout/stderr.
_ = w.Close()
e.Close("test end")
})
return e
}
type Expecter struct {
t *testing.T
out *stdbuf
@@ -84,7 +101,7 @@ func (e *Expecter) Rename(name string) {
e.name.Store(name)
}
func (e *Expecter) Close(reason string) error {
func (e *Expecter) Close(reason string) {
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
defer cancel()
@@ -94,6 +111,7 @@ func (e *Expecter) Close(reason string) error {
select {
case <-ctx.Done():
e.fatalf("close", "copy did not close in time")
return
case <-e.copyDone:
}
@@ -102,12 +120,11 @@ func (e *Expecter) Close(reason string) error {
select {
case <-ctx.Done():
e.fatalf("close", "log pipe did not close in time")
return
case <-e.logDone:
}
e.Logf("closed expecter")
return nil
}
func (e *Expecter) logClose(name string, c io.Closer) {
+55
View File
@@ -0,0 +1,55 @@
package testutil
import (
"io"
"testing"
"github.com/stretchr/testify/assert"
"gvisor.dev/gvisor/pkg/context"
"cdr.dev/slog/v3"
"github.com/coder/serpent"
)
// Writer wraps an underlying io.Writer and provides friendlier methods to write to it, including logging.
type Writer struct {
t *testing.T
w io.Writer
l slog.Logger
}
func NewWriterAttachedToInvocation(t *testing.T, logger slog.Logger, invocation *serpent.Invocation) *Writer {
r, w := io.Pipe()
invocation.Stdin = r
// Close the pipe at the end of the test to ensure any goroutine in the Invocation that reads from stdin won't leak.
t.Cleanup(func() {
_ = w.Close()
})
return &Writer{
t: t,
w: w,
l: logger,
}
}
func (w *Writer) Write(r rune) {
w.t.Helper()
_, err := w.w.Write([]byte{byte(r)})
if assert.NoError(w.t, err, "write failed") {
w.l.Debug(context.Background(), "wrote rune", slog.F("rune", r))
}
}
func (w *Writer) WriteLine(str string) {
w.t.Helper()
// Always write Windows style endings since our CLI prompt readers trim both out. Note this is *different* than what
// PTY-based tests do. On Unix-like operating systems we write a single carriage-return (\r) to delimit a line
// and the PTY translates it to a line feed (\n) for the CLI command to read. Here there is no translation.
newline := []byte{'\r', '\n'}
_, err := w.w.Write(append([]byte(str), newline...))
if assert.NoError(w.t, err, "write line failed") {
w.l.Debug(context.Background(), "wrote line", slog.F("line", str+string(newline)))
}
}