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

3 回复

供参考:

设计草案:一流的模糊测试

golang.org/s/draft-fuzzing-design

更多关于Golang中如何测试/模糊测试类ncurses的文本用户界面的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


我取得了进展,在此记录以供将来参考。

经过进一步研究,我意识到 github.com/gdamore/tcell(tview 依赖于此库)允许你创建一个模拟屏幕,你可以向其发送事件、捕获其状态等。而 tview 允许你覆盖其默认屏幕。因此,我创建并初始化了一个模拟屏幕,并用它设置了一个 tview.Application。我使用 protobuf 将模糊测试器的 []byte 解码为一个结构体,该结构体包含后端的初始状态以及任意数量的事件。这些事件可以是用户输入,也可以是后端有限的状态更改。

目前一切顺利;现在我需要充实代码的其他部分。

对于测试 tview 这类基于 ncurses 的文本用户界面,可以通过模拟终端输入和捕获输出来实现自动化。以下是一种使用 ptygoexpect 的解决方案,并提供了示例代码:

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 模拟了真实的终端交互环境。

回到顶部