Skip to content

Commit 32f0782

Browse files
committed
Add pty support for testing
Signed-off-by: apostasie <[email protected]>
1 parent 8e9e464 commit 32f0782

File tree

6 files changed

+65
-36
lines changed

6 files changed

+65
-36
lines changed

pkg/testutil/test/command.go

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,20 @@
1717
package test
1818

1919
import (
20+
"bytes"
2021
"fmt"
2122
"io"
2223
"os"
2324
"strings"
2425
"testing"
2526
"time"
2627

28+
"golang.org/x/sync/errgroup"
29+
"golang.org/x/term"
2730
"gotest.tools/v3/assert"
2831
"gotest.tools/v3/icmd"
32+
33+
"github.com/containerd/nerdctl/v2/pkg/testutil/test/internal/pty"
2934
)
3035

3136
const ExitCodeGenericFail = -1
@@ -49,6 +54,7 @@ type GenericCommand struct {
4954
stdin io.Reader
5055
async bool
5156
pty bool
57+
ptyWriters []func(*os.File) error
5258
timeout time.Duration
5359
workingDir string
5460

@@ -69,8 +75,9 @@ func (gc *GenericCommand) WithWrapper(binary string, args ...string) {
6975
gc.helperArgs = args
7076
}
7177

72-
func (gc *GenericCommand) WithPseudoTTY() {
78+
func (gc *GenericCommand) WithPseudoTTY(writers ...func(*os.File) error) {
7379
gc.pty = true
80+
gc.ptyWriters = writers
7481
}
7582

7683
func (gc *GenericCommand) WithStdin(r io.Reader) {
@@ -81,36 +88,58 @@ func (gc *GenericCommand) WithCwd(path string) {
8188
gc.workingDir = path
8289
}
8390

84-
// TODO: it should be possible to timeout execution
85-
// Primitives (gc.timeout) is here, it is just a matter of exposing a WithTimeout method
86-
// - UX to be decided
87-
// - validate use case: would we ever need this?
88-
8991
func (gc *GenericCommand) Run(expect *Expected) {
9092
if gc.t != nil {
9193
gc.t.Helper()
9294
}
9395

9496
var result *icmd.Result
9597
var env []string
96-
if gc.async {
97-
result = icmd.WaitOnCmd(gc.timeout, gc.result)
98-
env = gc.result.Cmd.Env
99-
} else {
98+
output := &bytes.Buffer{}
99+
stdout := ""
100+
errorGroup := &errgroup.Group{}
101+
var tty *os.File
102+
var psty *os.File
103+
if !gc.async {
100104
iCmdCmd := gc.boot()
101105
env = iCmdCmd.Env
102106

103107
if gc.pty {
104-
pty, tty, _ := Open()
108+
psty, tty, _ = pty.Open()
109+
_, _ = term.MakeRaw(int(tty.Fd()))
110+
105111
iCmdCmd.Stdin = tty
106112
iCmdCmd.Stdout = tty
107-
iCmdCmd.Stderr = tty
108-
defer pty.Close()
109-
defer tty.Close()
113+
114+
gc.result = icmd.StartCmd(iCmdCmd)
115+
116+
for _, writer := range gc.ptyWriters {
117+
_ = writer(psty)
118+
}
119+
120+
// Copy from the master
121+
errorGroup.Go(func() error {
122+
_, _ = io.Copy(output, psty)
123+
return nil
124+
})
125+
} else {
126+
// Run it
127+
gc.result = icmd.StartCmd(iCmdCmd)
110128
}
129+
}
130+
131+
result = icmd.WaitOnCmd(gc.timeout, gc.result)
132+
env = gc.result.Cmd.Env
133+
134+
if gc.pty {
135+
_ = tty.Close()
136+
_ = psty.Close()
137+
_ = errorGroup.Wait()
138+
}
111139

112-
// Run it
113-
result = icmd.RunCmd(iCmdCmd)
140+
stdout = result.Stdout()
141+
if stdout == "" {
142+
stdout = output.String()
114143
}
115144

116145
gc.rawStdErr = result.Stderr()
@@ -137,7 +166,7 @@ func (gc *GenericCommand) Run(expect *Expected) {
137166
}
138167
// Finally, check the output if we are asked to
139168
if expect.Output != nil {
140-
expect.Output(result.Stdout(), debug, gc.t)
169+
expect.Output(stdout, debug, gc.t)
141170
}
142171
}
143172
}

pkg/testutil/test/pty.go renamed to pkg/testutil/test/internal/pty/pty.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@
1414
limitations under the License.
1515
*/
1616

17-
package test
17+
package pty
1818

1919
import "errors"
2020

21-
var ErrPTYFailure = errors.New("pty failure")
22-
var ErrPTYUnsupportedPlatform = errors.New("pty not supported on this platform")
21+
var (
22+
ErrPTYFailure = errors.New("pty failure")
23+
ErrPTYUnsupportedPlatform = errors.New("pty not supported on this platform")
24+
)

pkg/testutil/test/pty_freebsd.go renamed to pkg/testutil/test/internal/pty/pty_freebsd.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
limitations under the License.
1515
*/
1616

17-
package test
17+
package pty
1818

1919
import (
2020
"os"

pkg/testutil/test/pty_linux.go renamed to pkg/testutil/test/internal/pty/pty_linux.go

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
limitations under the License.
1515
*/
1616

17-
package test
17+
package pty
1818

1919
import (
2020
"errors"
@@ -27,6 +27,7 @@ import (
2727
// Inspiration from https://github.com/creack/pty/tree/2cde18bfb702199728dd43bf10a6c15c7336da0a
2828

2929
func Open() (pty, tty *os.File, err error) {
30+
// Wrap errors
3031
defer func() {
3132
if err != nil && pty != nil {
3233
err = errors.Join(pty.Close(), err)
@@ -36,37 +37,33 @@ func Open() (pty, tty *os.File, err error) {
3637
}
3738
}()
3839

40+
// Open the pty
3941
pty, err = os.OpenFile("/dev/ptmx", os.O_RDWR, 0)
4042
if err != nil {
4143
return nil, nil, err
4244
}
4345

46+
// Get the slave unit number
4447
var n uint32
45-
err = ioctl(pty, syscall.TIOCGPTN, uintptr(unsafe.Pointer(&n)))
46-
if err != nil {
48+
_, _, e := syscall.Syscall(syscall.SYS_IOCTL, pty.Fd(), syscall.TIOCGPTN, uintptr(unsafe.Pointer(&n)))
49+
if e != 0 {
4750
return nil, nil, err
4851
}
4952

5053
sname := "/dev/pts/" + strconv.Itoa(int(n))
5154

55+
// Unlock
5256
var u int32
53-
err = ioctl(pty, syscall.TIOCSPTLCK, uintptr(unsafe.Pointer(&u)))
54-
if err != nil {
57+
_, _, e = syscall.Syscall(syscall.SYS_IOCTL, pty.Fd(), syscall.TIOCSPTLCK, uintptr(unsafe.Pointer(&u)))
58+
if e != 0 {
5559
return nil, nil, err
5660
}
5761

62+
// Open the slave, preventing it from becoming the controlling terminal
5863
tty, err = os.OpenFile(sname, os.O_RDWR|syscall.O_NOCTTY, 0)
5964
if err != nil {
6065
return nil, nil, err
6166
}
6267

6368
return pty, tty, nil
6469
}
65-
66-
func ioctl(f *os.File, cmd, ptr uintptr) error {
67-
_, _, e := syscall.Syscall(syscall.SYS_IOCTL, f.Fd(), cmd, ptr)
68-
if e != 0 {
69-
return e
70-
}
71-
return nil
72-
}

pkg/testutil/test/pty_windows.go renamed to pkg/testutil/test/internal/pty/pty_windows.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
limitations under the License.
1515
*/
1616

17-
package test
17+
package pty
1818

1919
import (
2020
"os"

pkg/testutil/test/test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package test
1818

1919
import (
2020
"io"
21+
"os"
2122
"testing"
2223
"time"
2324
)
@@ -98,7 +99,7 @@ type TestableCommand interface {
9899
// WithWrapper allows wrapping a command with another command (for example: `time`, `unbuffer`)
99100
WithWrapper(binary string, args ...string)
100101
// WithPseudoTTY
101-
WithPseudoTTY()
102+
WithPseudoTTY(writers ...func(*os.File) error)
102103
// WithStdin allows passing a reader to be used for stdin for the command
103104
WithStdin(r io.Reader)
104105
// WithCwd allows specifying the working directory for the command

0 commit comments

Comments
 (0)