Skip to content

Commit 7a0eaec

Browse files
committed
Add pseudo tty support for tests
Signed-off-by: apostasie <[email protected]>
1 parent 427f1cb commit 7a0eaec

File tree

3 files changed

+92
-0
lines changed

3 files changed

+92
-0
lines changed

pkg/testutil/test/command.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ type GenericCommand struct {
4545
envBlackList []string
4646
stdin io.Reader
4747
async bool
48+
pty bool
4849
timeout time.Duration
4950
workingDir string
5051

@@ -65,6 +66,10 @@ func (gc *GenericCommand) WithWrapper(binary string, args ...string) {
6566
gc.helperArgs = args
6667
}
6768

69+
func (gc *GenericCommand) WithPseudoTTY() {
70+
gc.pty = true
71+
}
72+
6873
func (gc *GenericCommand) WithStdin(r io.Reader) {
6974
gc.stdin = r
7075
}
@@ -77,6 +82,7 @@ func (gc *GenericCommand) WithCwd(path string) {
7782
// Primitives (gc.timeout) is here, it is just a matter of exposing a WithTimeout method
7883
// - UX to be decided
7984
// - validate use case: would we ever need this?
85+
8086
func (gc *GenericCommand) Run(expect *Expected) {
8187
if gc.t != nil {
8288
gc.t.Helper()
@@ -90,6 +96,16 @@ func (gc *GenericCommand) Run(expect *Expected) {
9096
} else {
9197
iCmdCmd := gc.boot()
9298
env = iCmdCmd.Env
99+
100+
if gc.pty {
101+
pty, tty, _ := Open()
102+
iCmdCmd.Stdin = tty
103+
iCmdCmd.Stdout = tty
104+
iCmdCmd.Stderr = tty
105+
defer pty.Close()
106+
defer tty.Close()
107+
}
108+
93109
// Run it
94110
result = icmd.RunCmd(iCmdCmd)
95111
}

pkg/testutil/test/pty.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package test
18+
19+
import (
20+
"errors"
21+
"os"
22+
"strconv"
23+
"syscall"
24+
"unsafe"
25+
)
26+
27+
// Inspiration from https://github.com/creack/pty/tree/2cde18bfb702199728dd43bf10a6c15c7336da0a
28+
29+
var ErrPTY = errors.New("pty failure")
30+
31+
func Open() (pty, tty *os.File, err error) {
32+
defer func() {
33+
if err != nil && pty != nil {
34+
err = errors.Join(pty.Close(), err)
35+
}
36+
if err != nil {
37+
err = errors.Join(ErrPTY, err)
38+
}
39+
}()
40+
41+
pty, err = os.OpenFile("/dev/ptmx", os.O_RDWR, 0)
42+
if err != nil {
43+
return nil, nil, err
44+
}
45+
46+
var n uint32
47+
err = ioctl(pty, syscall.TIOCGPTN, uintptr(unsafe.Pointer(&n)))
48+
if err != nil {
49+
return nil, nil, err
50+
}
51+
52+
sname := "/dev/pts/" + strconv.Itoa(int(n))
53+
54+
var u int32
55+
err = ioctl(pty, syscall.TIOCSPTLCK, uintptr(unsafe.Pointer(&u)))
56+
if err != nil {
57+
return nil, nil, err
58+
}
59+
60+
tty, err = os.OpenFile(sname, os.O_RDWR|syscall.O_NOCTTY, 0)
61+
if err != nil {
62+
return nil, nil, err
63+
}
64+
65+
return pty, tty, nil
66+
}
67+
68+
func ioctl(f *os.File, cmd, ptr uintptr) error {
69+
_, _, e := syscall.Syscall(syscall.SYS_IOCTL, f.Fd(), cmd, ptr)
70+
if e != 0 {
71+
return e
72+
}
73+
return nil
74+
}

pkg/testutil/test/test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ type TestableCommand interface {
9797
WithArgs(args ...string)
9898
// WithWrapper allows wrapping a command with another command (for example: `time`, `unbuffer`)
9999
WithWrapper(binary string, args ...string)
100+
// WithPseudoTTY
101+
WithPseudoTTY()
100102
// WithStdin allows passing a reader to be used for stdin for the command
101103
WithStdin(r io.Reader)
102104
// WithCwd allows specifying the working directory for the command

0 commit comments

Comments
 (0)