- serve summary lines at -v and -q, records and HideOnTTY, DSN labels - shutdown report: aborted requests fail the stop, a drained stop is nil - request log: request_id cap, default on, off by config or env, never the path for matched, 404 and 405 routes, component http; route:list --json - WorkerStats through runAttempt: done, failed, in flight, handed back - queue:clear and lagoon's production question on a pseudo-terminal: no cancels with exit 1, --force, -n and other environments skip it - Docker: PendingMigrations, migrate/rollback/deferred:purge --pretend, production runs off a terminal never read stdin, migrate:status --json - key:generate --json and -q
153 lines
4.4 KiB
Go
153 lines
4.4 KiB
Go
//go:build linux
|
|
|
|
package lagoon
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"syscall"
|
|
"testing"
|
|
"time"
|
|
"unsafe"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/bonfire"
|
|
)
|
|
|
|
// ptyOutput collects what the program writes to a pseudo-terminal.
|
|
type ptyOutput struct {
|
|
mu sync.Mutex
|
|
buf bytes.Buffer
|
|
}
|
|
|
|
func (p *ptyOutput) String() string {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
return p.buf.String()
|
|
}
|
|
|
|
// openPTY opens a pseudo-terminal pair: the slave is a terminal for the
|
|
// console (stdin and stdout), input written to the master reaches the
|
|
// slave's stdin, and everything the slave prints is collected.
|
|
func openPTY(t *testing.T) (master, slave *os.File, output *ptyOutput) {
|
|
t.Helper()
|
|
m, err := os.OpenFile("/dev/ptmx", os.O_RDWR|syscall.O_NOCTTY, 0)
|
|
if err != nil {
|
|
t.Fatalf("open /dev/ptmx: %v", err)
|
|
}
|
|
var unlock int32
|
|
if _, _, e := syscall.Syscall(syscall.SYS_IOCTL, m.Fd(), syscall.TIOCSPTLCK, uintptr(unsafe.Pointer(&unlock))); e != 0 {
|
|
t.Fatalf("unlock pty: %v", e)
|
|
}
|
|
var n uint32
|
|
if _, _, e := syscall.Syscall(syscall.SYS_IOCTL, m.Fd(), syscall.TIOCGPTN, uintptr(unsafe.Pointer(&n))); e != 0 {
|
|
t.Fatalf("pty number: %v", e)
|
|
}
|
|
s, err := os.OpenFile(fmt.Sprintf("/dev/pts/%d", n), os.O_RDWR|syscall.O_NOCTTY, 0)
|
|
if err != nil {
|
|
t.Fatalf("open pty slave: %v", err)
|
|
}
|
|
output = &ptyOutput{}
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
buf := make([]byte, 4096)
|
|
for {
|
|
n, err := m.Read(buf)
|
|
output.mu.Lock()
|
|
output.buf.Write(buf[:n])
|
|
output.mu.Unlock()
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
t.Cleanup(func() {
|
|
_ = s.Close()
|
|
_ = m.Close()
|
|
<-done
|
|
})
|
|
return m, s, output
|
|
}
|
|
|
|
// waitFor waits until the pty output contains want.
|
|
func waitFor(t *testing.T, output *ptyOutput, want string) {
|
|
t.Helper()
|
|
for range 200 {
|
|
if strings.Contains(output.String(), want) {
|
|
return
|
|
}
|
|
<-time.After(10 * time.Millisecond)
|
|
}
|
|
t.Fatalf("pty output lacks %q:\n%s", want, output.String())
|
|
}
|
|
|
|
// runOnPTY runs probe:confirm with args on a fresh pseudo-terminal after
|
|
// typing answer, and returns the exit code and the terminal output.
|
|
func runOnPTY(t *testing.T, env, answer string, args ...string) (int, *ptyOutput) {
|
|
t.Helper()
|
|
master, slave, output := openPTY(t)
|
|
if _, err := master.Write([]byte(answer)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
root, err := bonfire.NewRootIO("acme", []bonfire.Command{probeConfirm(envApp(t, env, ""))}, slave, slave, slave)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// --no-ansi keeps the line prompt (huh needs a terminal size).
|
|
code := bonfire.Execute(context.Background(), root, append([]string{"--no-ansi", "probe:confirm"}, args...))
|
|
return code, output
|
|
}
|
|
|
|
// TestConfirmProductionInteractiveNo: on an interactive terminal in
|
|
// production the question is asked; "n" or an empty answer prints the
|
|
// cancel line and exits 1 without an error block (Reported); "y" proceeds.
|
|
func TestConfirmProductionInteractiveNo(t *testing.T) {
|
|
for _, answer := range []string{"n\n", "\n", "no\n"} {
|
|
code, output := runOnPTY(t, "production", answer)
|
|
if code != bonfire.ExitFailure {
|
|
t.Fatalf("answer %q: exit %d\n%s", answer, code, output.String())
|
|
}
|
|
waitFor(t, output, "Migration cancelled.")
|
|
got := output.String()
|
|
if !strings.Contains(got, "Apply 3 migrations to the production database? [y/N]") {
|
|
t.Errorf("answer %q: question not asked:\n%s", answer, got)
|
|
}
|
|
if strings.Contains(got, "ERROR") || strings.Contains(got, "proceeded") {
|
|
t.Errorf("answer %q: error block or the step ran:\n%s", answer, got)
|
|
}
|
|
}
|
|
code, output := runOnPTY(t, "production", "y\n")
|
|
if code != bonfire.ExitOK {
|
|
t.Fatalf("answer y: exit %d\n%s", code, output.String())
|
|
}
|
|
waitFor(t, output, "proceeded")
|
|
}
|
|
|
|
// TestForceSkipsConfirm: --force, -n and any environment other than
|
|
// production proceed on an interactive terminal without asking (the typed
|
|
// "n" is never read).
|
|
func TestForceSkipsConfirm(t *testing.T) {
|
|
for name, tc := range map[string]struct {
|
|
env string
|
|
args []string
|
|
}{
|
|
"--force": {"production", []string{"--force"}},
|
|
"-n": {"production", []string{"-n"}},
|
|
"staging": {"staging", nil},
|
|
"development": {"development", nil},
|
|
} {
|
|
code, output := runOnPTY(t, tc.env, "n\n", tc.args...)
|
|
if code != bonfire.ExitOK {
|
|
t.Fatalf("%s: exit %d\n%s", name, code, output.String())
|
|
}
|
|
waitFor(t, output, "proceeded")
|
|
if strings.Contains(output.String(), "[y/N]") {
|
|
t.Errorf("%s: the question was asked:\n%s", name, output.String())
|
|
}
|
|
}
|
|
}
|