149 lines
5.0 KiB
Go
149 lines
5.0 KiB
Go
//go:build windows
|
||
|
||
package adminconsole
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"os"
|
||
"os/exec"
|
||
"strings"
|
||
"syscall"
|
||
"testing"
|
||
"time"
|
||
"unicode/utf16"
|
||
"unsafe"
|
||
|
||
"golang.org/x/sys/windows"
|
||
)
|
||
|
||
// 这是项目原生平台回归测试,不替代提升权限管道和生产交互验收。
|
||
// 子进程使用隐藏的独立控制台,避免读取或修改开发者正在使用的终端。
|
||
func TestWindowsTerminalNative(t *testing.T) {
|
||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||
defer cancel()
|
||
command := exec.CommandContext(ctx, os.Args[0], "-test.run=^TestWindowsTerminalNativeChild$", "-test.v", "-test.timeout=15s")
|
||
command.Env = append(os.Environ(), "SCERT_TEST_NATIVE_CONSOLE=1")
|
||
command.SysProcAttr = &syscall.SysProcAttr{CreationFlags: windows.CREATE_NEW_CONSOLE, HideWindow: true}
|
||
output, err := command.CombinedOutput()
|
||
if err != nil {
|
||
t.Fatalf("原生控制台回归失败:%v\n%s", err, output)
|
||
}
|
||
if !strings.Contains(string(output), "PASS: TestWindowsTerminalNativeChild") {
|
||
t.Fatalf("原生控制台子测试没有运行:%s", output)
|
||
}
|
||
}
|
||
|
||
func TestWindowsTerminalNativeChild(t *testing.T) {
|
||
if os.Getenv("SCERT_TEST_NATIVE_CONSOLE") != "1" {
|
||
t.Skip("仅由独立隐藏控制台中的子进程执行")
|
||
}
|
||
input, err := os.OpenFile("CONIN$", os.O_RDWR, 0)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer input.Close()
|
||
handle := windows.Handle(input.Fd())
|
||
var original uint32
|
||
if err := windows.GetConsoleMode(handle, &original); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := readConsoleInput.Find(); err != nil {
|
||
t.Fatalf("系统缺少所需的非阻塞控制台接口:%v", err)
|
||
}
|
||
flush := windows.NewLazySystemDLL("kernel32.dll").NewProc("FlushConsoleInputBuffer")
|
||
write := windows.NewLazySystemDLL("kernel32.dll").NewProc("WriteConsoleInputW")
|
||
screen := &windowsTerminal{input: handle, output: os.Stdout}
|
||
cases := []struct {
|
||
name string
|
||
keys string
|
||
want string
|
||
hidden bool
|
||
cancel bool
|
||
deadline bool
|
||
wantErr error
|
||
}{
|
||
{name: "visible", keys: "1\r", want: "1"},
|
||
{name: "secret", keys: "Native123\r", want: "Native123", hidden: true},
|
||
{name: "backspace", keys: "abc\bD\r", want: "abD", hidden: true},
|
||
{name: "surrogate", keys: "a\U0001F600\bb\r", want: "ab", hidden: true},
|
||
{name: "escape", keys: "\x1b", hidden: true, wantErr: context.Canceled},
|
||
{name: "interrupt", keys: "\x03", hidden: true, wantErr: context.Canceled},
|
||
{name: "cancel", hidden: true, cancel: true, wantErr: context.Canceled},
|
||
{name: "deadline", hidden: true, deadline: true, wantErr: context.DeadlineExceeded},
|
||
{name: "overflow", keys: strings.Repeat("a", 4097), hidden: true, wantErr: ErrInvalidRequest},
|
||
}
|
||
for _, test := range cases {
|
||
t.Run(test.name, func(t *testing.T) {
|
||
if ok, _, err := flush.Call(uintptr(handle)); ok == 0 {
|
||
t.Fatal(err)
|
||
}
|
||
if err := windows.SetConsoleMode(handle, original|windows.ENABLE_ECHO_INPUT|windows.ENABLE_LINE_INPUT); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
var before uint32
|
||
if err := windows.GetConsoleMode(handle, &before); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
limit := 2 * time.Second
|
||
if test.deadline {
|
||
limit = 250 * time.Millisecond
|
||
}
|
||
ctx, cancel := context.WithTimeout(context.Background(), limit)
|
||
defer cancel()
|
||
type result struct {
|
||
value string
|
||
err error
|
||
}
|
||
done := make(chan result, 1)
|
||
go func() {
|
||
value, err := screen.Read(ctx, "原生控制台测试", test.hidden)
|
||
done <- result{value, err}
|
||
}()
|
||
// 等待实际关闭回显后再输入,防止测试本身制造输入时序竞争。
|
||
for {
|
||
var active uint32
|
||
if err := windows.GetConsoleMode(handle, &active); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if active&windows.ENABLE_ECHO_INPUT == 0 {
|
||
break
|
||
}
|
||
select {
|
||
case result := <-done:
|
||
t.Fatalf("输入模式建立前读取已结束:%v", result.err)
|
||
case <-ctx.Done():
|
||
t.Fatal("未及时关闭控制台回显")
|
||
case <-time.After(time.Millisecond):
|
||
}
|
||
}
|
||
if test.cancel {
|
||
cancel()
|
||
} else if test.keys != "" {
|
||
units := utf16.Encode([]rune(test.keys))
|
||
records := make([]consoleRecord, len(units))
|
||
for index, unit := range units {
|
||
records[index] = consoleRecord{Type: 1, Down: 1, Repeat: 1, Character: unit}
|
||
}
|
||
var count uint32
|
||
ok, _, err := write.Call(uintptr(handle), uintptr(unsafe.Pointer(&records[0])), uintptr(len(records)), uintptr(unsafe.Pointer(&count)))
|
||
if ok == 0 || count != uint32(len(records)) {
|
||
t.Fatalf("写入原生控制台事件失败:%d %v", count, err)
|
||
}
|
||
}
|
||
select {
|
||
case result := <-done:
|
||
if !errors.Is(result.err, test.wantErr) || result.value != test.want {
|
||
t.Fatalf("读取结果不符:长度=%d,错误=%v", len(result.value), result.err)
|
||
}
|
||
case <-time.After(3 * time.Second):
|
||
t.Fatal("取消后控制台读取仍然阻塞")
|
||
}
|
||
var after uint32
|
||
if err := windows.GetConsoleMode(handle, &after); err != nil || after != before {
|
||
t.Fatalf("控制台模式未恢复:before=%x after=%x error=%v", before, after, err)
|
||
}
|
||
})
|
||
}
|
||
}
|