
CLI编程语言开发工具【免费下载链接】elvishPowerful scripting language versatile interactive shell项目地址https://gitcode.com/gh_mirrors/el/elvish点击查看免费下载本文基于 Elvish 项目作者 Qi Xiao 在 2024 年伦敦 Gophers 大会上的分享对应仓库内讲稿 website/slides/2024-09-london-gophers.md展开。文章以如何为你自己的编程语言写测试为主线完整还原 Elvish 测试方案的两次迭代演进从传统的表驱动测试到自研.elvtstranscript 文本 DSL再到用 VS Code 插件把手动抄写测试这一步彻底自动化。读者将掌握为什么解释器天然适合 transcript 式测试、.elvts文件的具体语法与运行机制、ELVISH_TRANSCRIPT_RUN编辑器协议的原理以及这套方案如何被复用到终端应用widget的测试上。一、背景Elvish 是什么它需要测试什么Elvish 既是一门编程语言也是一个现代化的交互式 shell详见仓库根目录 README.md 与演讲讲稿的 Intro 部分。它像 bash / zsh但更现代更强大的交互特性开箱即用就有语法高亮、Tab补全、Ctrl-L目录历史、Ctrl-R命令历史、Ctrl-N文件系统导航器可编程例如可以用一行 Elvish 代码定制提示符set edit:prompt { print (whoami)(tilde-abbr $pwd)$ }完整的编程语言列表[foo bar]、映射[keyvalue]、lambda{|x| ...}、并行遍历peach等真实语言特性可以直接用于 shell 脚本# [foo bar] - list # [keyvalue] - map var hosts [[namea cmdapt update] [nameb cmdpacman -Syu]] # peach parallel each # {|h| ...} - lambda peach {|h| ssh root$h[name] $h[cmd] } $hosts同时它保留了一切熟悉的 shell 能力——运行外部命令、管道、通配符还支持递归通配符vim main.go cat *.go | wc -l # Elvish also supports recursive wildcards cat **.go | wc -l这就带来了两个需要被测试的对象解释器把代码文本变成输出和终端应用接收键盘事件、渲染界面。演讲的核心正是围绕这两者设计一套低成本、高覆盖的测试体系。二、测试策略让写测试变得真正容易演讲先立下一个总原则测试很重要它让我们在修改代码时对正确性有信心但测试策略里最重要的一件事是让创建和维护测试变得极其容易容易写的测试 → 更多的测试 → 更高的测试覆盖率讲稿中给出 Elvish 达到92% 测试覆盖率。为什么解释器适合做这件事因为解释器的 API 极其简单输入一段代码输出文本 值。在 Elvish 里交互式会话天然就是输入一行代码、得到输出的序列~ echo hello world hello world ~ put [hello world] [foo bar] ▶ [hello world] ▶ [foo bar]注意值输出统一使用▶前缀这是 pkg/eval/evaltest/test_transcript.go 中valuePrefix定义的约定。既然 API 如此简单测试的表达形式就可以非常贴近真实使用场景——这正是后面一切设计的前提。三、迭代一表驱动测试table-driven tests第一个版本是最经典的做法把代码 期望输出写进一张 Go 表循环执行对比// Simplified interpreter API func Interpret(code string) ([]any, string) var tests []struct{ code string wantValues []any wantText string }{ {code: echo foo, wantText: foo\n}, } func TestInterpreter(t *testing.T) { for _, test : range tests { gotValues, gotText : Interpret(test.code) // Compare with test.wantValues and test.wantText } }此时添加一个测试用例需要三步实现新功能在终端里手动验证~ str:join , [a b] ▶ a,b把这次交互手工转换成 Go 测试用例{code: str:join , [a b], wantValues: []any{a,b}}问题显而易见第 3 步是纯机械劳动——而计算机最擅长重复劳动。四、迭代二transcript 测试——自研.elvtsDSL第二个版本的思想是既然第 3 步只是把终端 transcript翻译成表那不如直接以终端 transcript 本身作为测试的存储格式。于是就有了.elvtsElvish transcript文件把终端会话原样记录下来~ str:join , [a b] ▶ a,b然后用//go:embed把文件嵌入交给一个parseTranscripts解析器生成测试表//go:embed tests.elvts const transcripts string func TestInterpreter(t *testing.T) { tests : parseTranscripts(transcripts) for _, test : range tests { /* ... */ } }这背后的哲学是拥抱文本格式。虽然失去了严格的表结构但实践中完全无所谓——文本格式带来的是极低的创建与维护成本。4.1.elvts的完整语法仓库源码级.elvts的解析器实现在 pkg/transcript/transcript.go包注释本身就是一份完整的格式规范要点如下基本语法以 prompt~、/开头等开头的行被视为代码代码行可以延续到后续缩进与 prompt 对齐的行其余行视为输出。识别 prompt 的正则定义在 pkg/transcript/transcript.go#L400-L402// PromptPattern defines how to match prompts, used to determine which lines // start the code part of an interaction. var PromptPattern regexp.MustCompile(^[~/][^ ]* )例如下面的片段里echo lorem与缩进的echo ipsum属于同一次交互的两行代码lorem和ipsum是它的输出~ echo foo foo ~ echo lorem echo ipsum lorem ipsum标题与会话树支持# h1 #、## h2 ##、### h3 ###三级标题把 transcript 分割成一棵会话树标题就是会话名。解析逻辑在 pkg/transcript/transcript.go#L358-L368 的parseHeading中实现。注释与指令以//开头或全为/的行是注释会被忽略而以//开头但不是注释的行是指令directive只能出现在会话开头详见 pkg/transcript/transcript.go#L481-L489。.elv文件中的行内 transcript.elv源码文件里的 elvdoc 文档注释中可以包含elvish-transcript代码块每个代码块都是一个独立的 transcript命名为文件路径/符号名/代码块名。这一能力由 pkg/transcript/transcript.go#L197-L241 的parseElv实现——也就是说文档里的示例本身就是可执行的测试。4.2 运行机制evaltest包实际驱动这些测试运行的是 pkg/eval/evaltest 包。典型用法是每个测试包写一个入口函数见 pkg/eval/evaltest/test_transcript.go#L1-L17import ( embed src.elv.sh/pkg/eval/evaltest ) //go:embed *.elv *.elvts var transcripts embed.FS func TestTranscripts(t *testing.T) { evaltest.TestTranscriptsInFS(t, transcripts) }TestTranscriptsInFS会扫描 FS 中所有.elv与.elvts文件把每个 transcript 会话作为一个子测试运行。该包还内置了一套预设 setup 函数pkg/eval/evaltest/test_transcript.go#L271-L318可通过指令在 transcript 文件中直接引用指令作用//skip-test跳过该测试常用于.d.elv中仅供文档展示的示例//in-temp-dir在临时目录中运行//set-env $name $value设置环境变量//unset-env $name取消环境变量//eval $code先求值一段 Elvish 代码//only-on $cond类似//go:build约束满足条件才运行支持unix、32bit、64bit等标签//deprecation-level $x设置弃用告警级别默认指令只作用于当前会话加上each:前缀如//each:set-env则对全部子会话生效。为了可测试性该包还保证输出顺序的确定性当代码同时写值输出与字节输出、或同时写 stdout 与 stderr 时终端中顺序本无保证evalAndCollectOutputpkg/eval/evaltest/test_transcript.go#L371-L402会统一收集并规范化如剥离 SGR 颜色转义、统一换行符使测试结果可预期。仓库内真实用例可参考 e2e/script_test.elvts——它直接以终端会话的形式测试elvish -c和脚本文件执行////////////// # run script # ////////////// # with -c # ~ ./elvish -c echo hello from -c hello from -c # with file # ~ echo echo hello from file script.elv ./elvish script.elv hello from file4.3 剩余的问题复制粘贴仍是工作这个版本下添加测试用例变成实现新功能在终端手动验证把终端 transcript复制进tests.elvts。比起手写 Go 表已经轻松很多但复制这一步依然是纯机械工作——能不能连复制都省掉五、迭代 2.1VS Code 编辑器扩展——把测试写作变成编辑器操作答案是为.elvts文件写一个编辑器扩展运行光标处的代码并把输出自动插入到光标下方。仓库中的实现位于 vscode/src/transcript.ts注册了两个命令见 vscode/src/transcript.ts#L7-L14elvish.updateTranscriptOutputForCodeAtCursor更新光标处代码的输出elvish.openTranscriptPromptBelow在光标下方插入一个新的 prompt 行。5.1 更新输出ELVISH_TRANSCRIPT_RUN协议updateTranscriptOutputForCodeAtCursor的完整流程vscode/src/transcript.ts#L22-L70先保存当前文档测试运行的是磁盘上的内容以当前文件与光标行号设置环境变量并运行测试const { error, stdout } await exec( go test -run TestTranscripts, { cwd: dir, env: { ...process.env, ELVISH_TRANSCRIPT_RUN: ${base}:${lineno} }, });若测试失败从输出中匹配UPDATE (.*)$解析出一段 JSON其中包含需要替换的行区间与新内容const { fromLine, toLine, content } JSON.parse(match[1]) as UpdateInstruction;用编辑器 API 把[fromLine, toLine)区间的内容替换为新输出。这段机器可读的UPDATE指令正是由evaltest包产生的。协议定义在 pkg/eval/evaltest/test_transcript.go#L75-L106设置ELVISH_TRANSCRIPT_RUN文件名:行号后只运行该交互所属的会话且只运行到该交互为止若实际输出与文件内容不符测试失败并输出一段机器可读的更新指令。例如对下面这段第 12 行起12 ~ echo foo 13 echo bar 14 lorem 15 ipsum运行env ELVISH_TRANSCRIPT_RUNfoo_test.elvts:12 go test -run TestTranscripts会得到行号区间左闭右开UPDATE {fromLine: 14, toLine: 16, content: foo\nbar\n}一个实现细节值得注意VS Code 的行号是0-based而ELVISH_TRANSCRIPT_RUN协议与UPDATE指令都是1-based因此扩展里做了显式换算vscode/src/transcript.ts#L28-L31 的注释专门说明了这一点。5.2 插入新交互openTranscriptPromptBelowopenTranscriptPromptBelowvscode/src/transcript.ts#L85-L124做的事情是从光标所在行向上搜索最近的 prompt 或标题行确定当前使用的 prompt 样式默认~向下找到第一个标题/prompt 行确定插入位置在光标下方插入一行新的 prompt并把光标移动到 prompt 之后。配合这两个命令开发流程变成讲稿的 demo 部分实现新功能直接在编辑器的tests.elvts里手动输入并运行~ use str ~ str:join , [a b] ▶ a,b输出由扩展自动补全。至此写测试作为开发中的一个独立步骤被彻底消除——测试与开发合而为一。六、附注外部测试文件的依赖注入技巧testexport演讲中穿插了一个与 transcript 无关、但非常实用的 Go 测试技巧。经典的依赖注入写法是把包级变量留一个可替换的口子// in foo.go package foo var stdout os.Stdout func Hello() { fmt.Fprintln(stdout, Hello!) } // in foo_test.go package foo func TestHello(t *testing.T) { stdout ... ... }但如果测试是外部测试包package foo_test呢直接导出stdout会让它成为公开 API 的一部分并不理想。解法是再引入一个仅供内部测试用的文件// foo.go is unchanged // in testexport_test.go package foo // an internal test file var Stdout stdout // in foo_test.go package foo_test // an external test file func TestHello(t *testing.T) { *foo.Stdout ... ... }内部测试文件testexport_test.go属于package foo可以把私有变量的指针导出外部测试包通过指针间接改写而不污染公共 API。这个模式在 Elvish 仓库里被大量使用。例如 pkg/eval/testexport_test.go 导出了一批可变指针package eval // Pointers to variables that can be mutated for testing. var ( GetHome getHome Getwd getwd OSExit osExit TimeAfter timeAfter TimeNow timeNow NextEvalCount nextEvalCount ... )仓库中类似的testexport文件还出现在 pkg/edit/highlight/testexport_test.go、pkg/md/testexport_test.go、pkg/mods/platform/testexport_test.go 等处读者可以直接查阅这些文件了解完整用法。七、把同样的方法论用到终端应用widget 测试解释器解决了接下来是终端应用交互界面的测试。7.1 Widget 抽象像 GUI 应用一样Elvish 的终端应用由一系列widget组成。抽象定义在 pkg/cli/tk/widget.go#L11-L17// Widget is the basic component of UI; it knows how to handle events and how to // render itself. type Widget interface { Renderer MaxHeighter Handler }即一个 widget 由三件事构成Render(width, height int) *term.Buffer把自身渲染成一个BufferMaxHeight(width, height int) int渲染所需的最大高度Handle(event term.Event) bool处理一个终端事件返回是否已处理。其中Buffer存储富文本带样式的文本与光标位置Event键盘事件以及其他事件。以CodeArea代码输入区实现见 pkg/cli/tk/codearea.go为例保存文本内容与光标位置Render输出包含当前内容与光标的BufferHandle处理各类按键按a插入a按Backspace删除光标左侧字符按Left光标左移。7.2 为什么 widget 测试很啰嗦widget 的 API 同样简单输入Event输出Buffer。但输入输出常常是多个、且交错的——一个典型的测试流程是按x、按y渲染并检查按Left渲染并检查按Backspace渲染并检查。如果用传统断言硬写测试会变得冗长且难写。7.3 解法给 widget 创建 Elvish 绑定然后……用 transcript 测试演讲给出的思路为 widget 暴露一组 Elvish 绑定比如send发送事件、render渲染输出于是测试就退化成我们已经有完整工具的 transcript 测试~ send [x y]; render xy ~ send [Left]; render xy ~ send [Backspace]; render y这看起来就像截图测试screenshot tests——只不过截图渲染结果直接内嵌在测试文件里无需外部图片资源、无需视觉对比工具。7.4 编码文本样式与光标位置真实的render输出比上面更精细一些需要把文本样式与光标位置也编码进文本。演讲展示了实际的渲染结果R/G等字母代表不同样式̅是光标标记~ send [e c o]; render ┌────────────────────────────────────────┐ │eco │ │RRR ̅̂ │ └────────────────────────────────────────┘ ~ send [Left]; render ┌────────────────────────────────────────┐ │eco │ │RRR̅̂ │ └────────────────────────────────────────┘ ~ send [h]; render ┌────────────────────────────────────────┐ │echo │ │GGGG̅̂ │ └────────────────────────────────────────┘这种用单字符代表样式、把光标内联进文本的编码方式与仓库中 pkg/cli/clitest/apptest.go 的测试设施一脉相承Styles定义了一张字符 → 样式映射表如_下划线、b粗体、v绿色、c青色表示注释等见 pkg/cli/clitest/apptest.go#L12-L28Fixture.MakeBuffer与TestTTY则负责把这种带样式的文本标记行构建成预期的Buffer并比对pkg/cli/clitest/apptest.go#L79-L87。7.5 还能更简单吗至此测试仍然需要手动誊写测试会话。演讲在结尾提出了下一个演进方向能否直接录制真实的 TUI 会话——即把真实终端里的操作与渲染自动录制成 transcript。这为测试体系留出了一个明确的下一步想象空间。八、结论Elvish 测试策略的三条启示让测试容易测试数量与测试成本直接相关成本越低覆盖越高拥抱文本文本格式虽然结构松散但换来的是极低的创建与维护成本该思路的先行者是 Mercurial 的测试体系讲稿中提到了这一 prior art拥抱编辑器把生成测试从人工流程变成编辑器内的一键操作让写测试融入日常开发而不是额外负担。这套方法论的价值不限于 Elvish任何代码进、输出出的组件解释器、编译器、REPL、甚至 widget 化的 TUI都可以套用——先定义贴近真实会话的文本格式再用编辑器插件把格式的维护自动化。九、继续深入仓库若想进一步研究本文涉及的实现可以从以下路径入手讲稿原文website/slides/2024-09-london-gophers.mdtranscript 格式规范与解析器pkg/transcript/transcript.gotranscript 测试运行器与ELVISH_TRANSCRIPT_RUN协议pkg/eval/evaltest/test_transcript.goVS Code 扩展实现vscode/src/transcript.ts终端 widget 抽象pkg/cli/tk/widget.goCLI 测试设施FakeTTY / Fixture / 样式编码pkg/cli/clitest/apptest.go外部测试依赖注入示例pkg/eval/testexport_test.go真实 transcript 测试文件e2e/script_test.elvts赞分享CLI编程语言开发工具【免费下载链接】elvishPowerful scripting language versatile interactive shell项目地址https://gitcode.com/gh_mirrors/el/elvish点击查看免费下载相关推荐templ Elements 深度指南在 Go 组件中渲染 HTML 元素templ Elements 深度指南在 Go 组件中渲染 HTML 元素 templ 是一种用 Go 编写 HTML 用户界面的语言。在 templ 组件中CLI编程语言开发工具LibreChat测试贡献编写测试用例与提高测试覆盖率LibreChat测试贡献编写测试用例与提高测试覆盖率 引言 在开源项目开发中测试是确保代码质量和稳定性的关键环节。LibreChat作为一个功能丰富的Ch人工智能大模型AI 应用交互助手Dayz-Cheat-H4ck-A1mbot常见问题从编译错误到游戏崩溃的解决方案Dayz Cheat H4ck A1mbot常见问题从编译错误到游戏崩溃的解决方案 Dayz Cheat H4ck A1mbot是一个基于C开发的DayZ上一篇Blender形状键保护插件SKkeeper终极使用指南如何应用修改器不丢失形状键下一篇天龙八部单机版GM工具TlbbGmTool完整使用指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考