Golang中如何测试/模糊测试类ncurses的文本用户界面
Golang中如何测试/模糊测试类ncurses的文本用户界面
我正在开发一个使用 github.com/rivo/tview 向用户展示一些配置选项的程序。我已经浏览了用户界面,目前看起来一切正常。
然而,我需要自动化这些测试。这里显而易见的选择是 expect 或提供此功能的某个 Go 包,例如 github.com/google/goexpect。让 goexpect 与 tview 协同工作是一场我尚未完全取胜的战斗;tview 的 Application.Run() 不再立即返回,这是一个改进,但到目前为止,我发送的转义序列还未能实现任何导航。
你会如何进行类似 ncurses 用户界面的自动化测试?我倾向于使用 Go 的解决方案,但也不排斥非 Go 的解决方案。
我目前正在使用 goexpect 和 github.com/creack/pty。一些搜索让我找到了 github.com/Azure/go-ansiterm 及其导入者,例如 github.com/kriskowal/cops/vtio——但我还不确定它们是否有帮助。
一旦测试可以实现自动化,我希望能将其连接到 dvyukov 的 go-fuzz。我想使用模糊测试来查找可能导致挂起或崩溃的任何输入序列。目前我重新执行 os.Args[0] 来连接到 pty,不过如果可能的话,我怀疑避免 exec 会大大加快模糊测试的速度。另外,我怀疑用于跟踪模糊测试覆盖率的机制会被 exec 破坏……
更多关于Golang中如何测试/模糊测试类ncurses的文本用户界面的实战教程也可以访问 https://www.itying.com/category-94-b0.html
更多关于Golang中如何测试/模糊测试类ncurses的文本用户界面的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
我取得了进展,在此记录以供将来参考。
经过进一步研究,我意识到 github.com/gdamore/tcell(tview 依赖于此库)允许你创建一个模拟屏幕,你可以向其发送事件、捕获其状态等。而 tview 允许你覆盖其默认屏幕。因此,我创建并初始化了一个模拟屏幕,并用它设置了一个 tview.Application。我使用 protobuf 将模糊测试器的 []byte 解码为一个结构体,该结构体包含后端的初始状态以及任意数量的事件。这些事件可以是用户输入,也可以是后端有限的状态更改。
目前一切顺利;现在我需要充实代码的其他部分。
对于测试 tview 这类基于 ncurses 的文本用户界面,可以通过模拟终端输入和捕获输出来实现自动化。以下是一种使用 pty 和 goexpect 的解决方案,并提供了示例代码:
package main
import (
"fmt"
"io"
"os"
"testing"
"time"
"github.com/creack/pty"
"github.com/google/goexpect"
"github.com/rivo/tview"
)
// 启动 tview 应用并连接到 pty
func startTViewApp(ptyFile *os.File) error {
app := tview.NewApplication()
textView := tview.NewTextView().
SetText("配置选项界面\n按 Tab 导航\n按 Enter 确认")
app.SetRoot(textView, true)
// 将应用输出重定向到 pty
app.SetScreen(tview.NewTestScreen(ptyFile))
// 在 goroutine 中运行应用
go func() {
if err := app.Run(); err != nil {
fmt.Printf("应用错误: %v\n", err)
}
}()
return nil
}
// 测试函数示例
func TestTViewNavigation(t *testing.T) {
// 创建主从 pty
master, slave, err := pty.Open()
if err != nil {
t.Fatalf("创建 pty 失败: %v", err)
}
defer master.Close()
defer slave.Close()
// 启动 tview 应用
if err := startTViewApp(slave); err != nil {
t.Fatalf("启动应用失败: %v", err)
}
// 创建 expect 实例
expecter, _, err := goexpect.SpawnWithArgs(
[]string{"cat"},
time.Second*5,
goexpect.Tee(master),
goexpect.Verbose(true),
)
if err != nil {
t.Fatalf("创建 expect 失败: %v", err)
}
defer expecter.Close()
// 发送 Tab 键进行导航
_, err = expecter.ExpectBatch([]goexpect.Batcher{
&goexpect.BExp{R: "配置选项界面"},
&goexpect.BSnd{S: "\t"},
&goexpect.BExp{R: ".*"}, // 匹配任意输出
}, time.Second*2)
if err != nil {
t.Errorf("导航测试失败: %v", err)
}
// 发送 Enter 键
_, err = expecter.Send("\n")
if err != nil {
t.Errorf("发送 Enter 失败: %v", err)
}
}
// 模糊测试示例
func FuzzTViewInput(f *testing.F) {
// 添加初始测试用例
testCases := []string{
"\t", // Tab
"\n", // Enter
"\x1b[A", // 上箭头
"\x1b[B", // 下箭头
}
for _, tc := range testCases {
f.Add(tc)
}
f.Fuzz(func(t *testing.T, input string) {
master, slave, err := pty.Open()
if err != nil {
t.Skipf("创建 pty 失败: %v", err)
}
defer master.Close()
defer slave.Close()
// 启动应用
app := tview.NewApplication()
textView := tview.NewTextView()
app.SetRoot(textView, true)
app.SetScreen(tview.NewTestScreen(slave))
done := make(chan error, 1)
go func() {
done <- app.Run()
}()
// 发送模糊输入
_, err = master.Write([]byte(input))
if err != nil {
t.Logf("写入输入失败: %v", err)
}
// 等待并检查应用状态
select {
case err := <-done:
if err != nil {
t.Errorf("应用异常退出: %v", err)
}
case <-time.After(100 * time.Millisecond):
// 应用仍在运行,正常停止
app.Stop()
}
})
}
对于避免 exec 以加速模糊测试,可以使用进程内测试。以下示例展示了如何直接调用应用逻辑:
// 进程内模糊测试
func FuzzTViewInProcess(f *testing.F) {
f.Fuzz(func(t *testing.T, input []byte) {
// 创建虚拟终端
master, slave, err := pty.Open()
if err != nil {
t.Skipf("创建 pty 失败: %v", err)
}
defer master.Close()
defer slave.Close()
// 直接调用应用逻辑
app := tview.NewApplication()
form := tview.NewForm()
app.SetRoot(form, true)
// 使用测试屏幕
screen := tview.NewTestScreen(slave)
app.SetScreen(screen)
// 在 goroutine 中运行
done := make(chan error, 1)
go func() {
done <- app.Run()
}()
// 发送模糊输入
for _, b := range input {
// 模拟按键事件
event := tcell.NewEventKey(tcell.Key(b), rune(b), tcell.ModNone)
screen.PostEvent(event)
}
// 清理
select {
case <-time.After(50 * time.Millisecond):
app.Stop()
case err := <-done:
if err != nil {
t.Errorf("应用错误: %v", err)
}
}
})
}
对于覆盖率跟踪,确保在测试命令中启用覆盖率收集:
go test -fuzz=Fuzz -coverprofile=coverage.out ./...
这种方法避免了外部进程执行,保持了覆盖率跟踪的有效性,同时通过 pty 模拟了真实的终端交互环境。

