Skip to content

Commit

Permalink
testscript: add pty/ptyout commands
Browse files Browse the repository at this point in the history
  • Loading branch information
FiloSottile committed Jan 2, 2023
1 parent fef0545 commit d43ebe7
Show file tree
Hide file tree
Showing 9 changed files with 230 additions and 15 deletions.
41 changes: 41 additions & 0 deletions testscript/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"os/exec"
"path/filepath"
"regexp"
"runtime"
"strconv"
"strings"

Expand Down Expand Up @@ -41,6 +42,8 @@ var scriptCmds = map[string]func(*TestScript, bool, []string){
"stderr": (*TestScript).cmdStderr,
"stdin": (*TestScript).cmdStdin,
"stdout": (*TestScript).cmdStdout,
"pty": (*TestScript).cmdPty,
"ptyout": (*TestScript).cmdPtyout,
"stop": (*TestScript).cmdStop,
"symlink": (*TestScript).cmdSymlink,
"unix2dos": (*TestScript).cmdUNIX2DOS,
Expand Down Expand Up @@ -191,6 +194,10 @@ func (ts *TestScript) cmdCp(neg bool, args []string) {
src = arg
data = []byte(ts.stderr)
mode = 0o666
case "ptyout":
src = arg
data = []byte(ts.ptyout)
mode = 0o666
default:
src = ts.MkAbs(arg)
info, err := os.Stat(src)
Expand Down Expand Up @@ -395,6 +402,9 @@ func (ts *TestScript) cmdStdin(neg bool, args []string) {
if len(args) != 1 {
ts.Fatalf("usage: stdin filename")
}
if ts.stdinPty {
ts.Fatalf("conflicting use of 'stdin' and 'pty -stdin'")
}
ts.stdin = ts.ReadFile(args[0])
}

Expand All @@ -414,6 +424,37 @@ func (ts *TestScript) cmdGrep(neg bool, args []string) {
scriptMatch(ts, neg, args, "", "grep")
}

func (ts *TestScript) cmdPty(neg bool, args []string) {
if runtime.GOOS == "windows" {
ts.Fatalf("unsupported: pty on Windows")
}
if neg {
ts.Fatalf("unsupported: ! pty")
}
switch len(args) {
case 1:
ts.ptyin = ts.ReadFile(args[0])
case 2:
if args[0] != "-stdin" {
ts.Fatalf("usage: pty [-stdin] filename")
}
if ts.stdin != "" {
ts.Fatalf("conflicting use of 'stdin' and 'pty -stdin'")
}
ts.stdinPty = true
ts.ptyin = ts.ReadFile(args[1])
default:
ts.Fatalf("usage: pty [-stdin] filename")
}
if ts.ptyin == "" {
ts.Fatalf("pty input file is empty")
}
}

func (ts *TestScript) cmdPtyout(neg bool, args []string) {
scriptMatch(ts, neg, args, ts.ptyout, "pty")
}

// stop stops execution of the test (marking it passed).
func (ts *TestScript) cmdStop(neg bool, args []string) {
if neg {
Expand Down
10 changes: 10 additions & 0 deletions testscript/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,16 @@ The predefined commands are:
Apply the grep command (see above) to the standard output
from the most recent exec or wait command.
- pty [-stdin] file
Attach the next exec command to a controlling pseudo-terminal, and use the
contents of the given file as the raw terminal input. If -stdin is specified,
also attach the terminal to standard input.
Note that this does n ot attach the terminal to standard output/error.
- [!] ptyout [-count=N] pattern
Apply the grep command (see above) to the raw controlling terminal output
from the most recent exec command.
- stop [message]
Stop the test early (marking it as passing), including the message if given.
Expand Down
8 changes: 0 additions & 8 deletions testscript/envvarname.go

This file was deleted.

7 changes: 0 additions & 7 deletions testscript/envvarname_windows.go

This file was deleted.

31 changes: 31 additions & 0 deletions testscript/pty_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package testscript

import (
"bytes"
"os"
"syscall"
"unsafe"
)

func ptyName(f *os.File) (string, error) {
const IOC_PARAM_SHIFT = 13
const IOC_PARAM_MASK = (1 << IOC_PARAM_SHIFT) - 1
const TIOCPTYGNAME_PARM_LEN = (syscall.TIOCPTYGNAME >> 16) & IOC_PARAM_MASK
out := make([]byte, TIOCPTYGNAME_PARM_LEN)

err := ioctl(f, syscall.TIOCPTYGNAME, uintptr(unsafe.Pointer(&out[0])))
if err != nil {
return "", err
}

i := bytes.IndexByte(out, 0x00)
return string(out[:i]), nil
}

func ptyGrant(f *os.File) error {
return ioctl(f, syscall.TIOCPTYGRANT, 0)
}

func ptyUnlock(f *os.File) error {
return ioctl(f, syscall.TIOCPTYUNLK, 0)
}
26 changes: 26 additions & 0 deletions testscript/pty_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package testscript

import (
"os"
"strconv"
"syscall"
"unsafe"
)

func ptyName(f *os.File) (string, error) {
var out uint
err := ioctl(f, syscall.TIOCGPTN, uintptr(unsafe.Pointer(&out)))
if err != nil {
return "", err
}
return "/dev/pts/" + strconv.Itoa(int(out)), nil
}

func ptyGrant(f *os.File) error {
return nil
}

func ptyUnlock(f *os.File) error {
var zero int
return ioctl(f, syscall.TIOCSPTLCK, uintptr(unsafe.Pointer(&zero)))
}
58 changes: 58 additions & 0 deletions testscript/pty_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//go:build !windows
// +build !windows

package testscript

import (
"os"
"os/exec"
"syscall"
)

func setCtty(cmd *exec.Cmd, tty *os.File) error {
cmd.SysProcAttr = &syscall.SysProcAttr{}
cmd.SysProcAttr.Setctty = true
cmd.SysProcAttr.Setsid = true
cmd.SysProcAttr.Ctty = 3
cmd.ExtraFiles = []*os.File{tty}
return nil
}

func ptyOpen() (pty, tty *os.File, err error) {
p, err := os.OpenFile("/dev/ptmx", os.O_RDWR, 0)
if err != nil {
return nil, nil, err
}

name, err := ptyName(p)
if err != nil {
p.Close()
return nil, nil, err
}

if err := ptyGrant(p); err != nil {
p.Close()
return nil, nil, err
}

if err := ptyUnlock(p); err != nil {
p.Close()
return nil, nil, err
}

t, err := os.OpenFile(name, os.O_RDWR|syscall.O_NOCTTY, 0)
if err != nil {
p.Close()
return nil, nil, err
}

return p, t, nil
}

func ioctl(f *os.File, cmd, ptr uintptr) error {
_, _, err := syscall.Syscall(syscall.SYS_IOCTL, f.Fd(), cmd, ptr)
if err != 0 {
return err
}
return nil
}
15 changes: 15 additions & 0 deletions testscript/pty_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package testscript

import (
"errors"
"os"
"os/exec"
)

func setCtty(cmd *exec.Cmd, tty *os.File) error {
return errors.New("setctty unsupported on Windows")
}

func ptyOpen() (pty, tty *os.File, err error) {
return nil, nil, errors.New("pty unsupported on Windows")
}
49 changes: 49 additions & 0 deletions testscript/testscript.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ package testscript
import (
"bytes"
"context"
"errors"
"flag"
"fmt"
"go/build"
"io"
"io/ioutil"
"os"
"os/exec"
Expand Down Expand Up @@ -84,6 +86,13 @@ func (e *Env) Getenv(key string) string {
return ""
}

func envvarname(k string) string {
if runtime.GOOS == "windows" {
return strings.ToLower(k)
}
return k
}

// Setenv sets the value of the environment variable named by the key. It
// panics if key is invalid.
func (e *Env) Setenv(key, value string) {
Expand Down Expand Up @@ -293,6 +302,9 @@ type TestScript struct {
stdin string // standard input to next 'go' command; set by 'stdin' command.
stdout string // standard output from last 'go' command; for 'stdout' command
stderr string // standard error from last 'go' command; for 'stderr' command
ptyin string // terminal input; set by 'pty' command
stdinPty bool // connect pty to standard input; set by 'pty -stdin' command
ptyout string // terminal output; for 'ptyout' command
stopped bool // test wants to stop early
start time.Time // time phase started
background []backgroundCmd // backgrounded 'exec' and 'go' commands
Expand Down Expand Up @@ -692,16 +704,51 @@ func (ts *TestScript) exec(command string, args ...string) (stdout, stderr strin
var stdoutBuf, stderrBuf strings.Builder
cmd.Stdout = &stdoutBuf
cmd.Stderr = &stderrBuf
if ts.ptyin != "" {
pty, tty, err := ptyOpen()
if err != nil {
return "", "", err
}
doneR, doneW := make(chan struct{}), make(chan struct{})
var ptyBuf strings.Builder
go func() {
io.Copy(pty, strings.NewReader(ts.ptyin))
pty.Write([]byte{4 /* EOT */})
close(doneW)
}()
go func() {
io.Copy(&ptyBuf, pty)
close(doneR)
}()
defer func() {
tty.Close()
pty.Close()
<-doneR
<-doneW
ts.ptyin = ""
ts.ptyout = ptyBuf.String()
}()
if err := setCtty(cmd, tty); err != nil {
return "", "", err
}
if ts.stdinPty {
cmd.Stdin = tty
}
}
if err = cmd.Start(); err == nil {
err = ctxWait(ts.ctxt, cmd)
}
ts.stdin = ""
ts.stdinPty = false
return stdoutBuf.String(), stderrBuf.String(), err
}

// execBackground starts the given command line (an actual subprocess, not simulated)
// in ts.cd with environment ts.env.
func (ts *TestScript) execBackground(command string, args ...string) (*exec.Cmd, error) {
if ts.ptyin != "" {
return nil, errors.New("pty is not supported by background commands")
}
cmd, err := ts.buildExecCmd(command, args...)
if err != nil {
return nil, err
Expand Down Expand Up @@ -817,6 +864,8 @@ func (ts *TestScript) ReadFile(file string) string {
return ts.stdout
case "stderr":
return ts.stderr
case "ptyout":
return ts.ptyout
default:
file = ts.MkAbs(file)
data, err := ioutil.ReadFile(file)
Expand Down

0 comments on commit d43ebe7

Please sign in to comment.