ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Erlang/OTP Common Test 的 ct_doctest 指南:让 Markdown 文档示例成为可执行的自动化测试

Erlang/OTP Common Test 的 ct_doctest 指南:让 Markdown 文档示例成为可执行的自动化测试 编程语言语言运行时标准库编译器并发编程【免费下载链接】otpErlang/OTP项目地址https://gitcode.com/gh_mirrors/ot/otp点击查看免费下载ct_doctest是 Erlang/OTP Common Test 在 OTP 29.0 中引入的文档测试工具它从模块文档EEP-48 文档属性或独立 Markdown 文件中提取形如 Erlang Shell 会话的代码块将其作为自动化测试运行从而保证文档示例始终正确、与代码同步且风格一致。阅读本文后你将掌握 doctest 的提示符语法规则、module/1,2,3与file/1,2,3的完整 API 用法、预绑定变量与异常匹配技巧以及各选项parser、skipped_blocks、missing_tests、skip_tests、verbose的底层实现原理。什么是 ct_doctestct_doctest的核心目标只有一个验证文档中的代码示例真实可运行。在 ct_doctest.erl 的模块文档中作者明确写道使用ct_doctest可以确保文档示例正确、不过时、风格一致correct, up to date, and stylistically consistent。它的工作方式非常简单直观从两类来源中寻找代码块——模块通常使用 文档属性 编写或独立文件默认使用内置的 Markdown 解析器提取代码块只运行看起来像 Shell 会话的 Erlang 代码块把代码块当作测试逐条执行并将实际输出与文档中写好的期望输出逐字比对。在 Common Test 测试套件中的典型用法是把某个模块的文档示例整体拉进来测试all() - [doctests]. doctests(_Config) - ct_doctest:module(my_module).也可以直接对独立 Markdown 文件运行doctests(_Config) - ct_doctest:file(doc/example.md).提示符格式规则Prompt Format Rulesdoctest 解析器识别的示例必须模拟 Erlang Shell 会话提示符形如N其中N在每个代码块内从1开始。期望输出写在提示符行的后续行上。完整规则如下来自 ct_doctest.erl 的模块文档每个代码块的提示符必须从1开始后续提示符必须依次递增2、3、……续行必须缩进提示符块内允许%风格的注释行提示符编号不匹配会直接导致 doctest 解析错误。最基础的示例1 12. 3提示符编号校验由check_prompt_numbers/2实现ct_doctest.erl它从1开始逐个比对编号一旦发现跳号就会抛出形如Bad prompt number 3; expected 2的错误。在 ct_doctest_SUITE.erl 的 parser_prompt_parsing 用例 中可以找到对这类错误的断言验证。支持格式一览从入门到进阶解析器非常灵活支持多种书写风格。以下示例均来自 ct_doctest.erl 的模块文档可逐条验证。基础示例普通代码块与 erlang 代码块普通 Markdown 代码块与显式标注erlang的代码块都受支持二者等价erlang 1 12. 3 1 12. 3 多行提示符续行表达式跨越多行时用开头的提示符行接续续行缩进1 1 2 . 3同一提示符内的多个表达式用逗号分隔多个表达式1 A 1, A 2. 3跨多行的期望输出期望输出本身也可以跨行1 [1, 2]. [ 1 , 2 ]多提示符示例1 1 2. 3 2 3 4. 7变量定义与传递代码块中定义的任何变量都会在后续提示符中可用1 A 1 2. 3 2 A 3. 6忽略某个结果如果不想校验某条提示符的输出直接不写期望输出即可1 1 2. 2 3 4. 7这里第一条1 2.的结果被忽略只有第二条被断言。注释%%注释可以插入代码块的任意位置包括提示符之间、匹配结果之间甚至异常输出中间但字符串内部的%不是注释%% A comment before the first prompt 1 [1, %% A comment between prompts 2]. [1, %% A comment in a match 2] 2 [1, %% Indented comment between prompts 2]. [1, %% Indented comment in a match 2]注释剥离逻辑见 strip_comments/1它会过滤掉以空白加%开头的行。匹配 Map使用而非:匹配 Map 时可以使用 Shell 语法即关联符号而非普通 Erlang 代码中的:1 #{ a b }. #{ a b }这一点的底层实现很有意思rewrite_match_ast/1 会遍历匹配模式的 AST把所有map_field_assoc节点改写成map_field_exact:从而让 Shell 风格的 Map 字面量能作为匹配模式参与比对。用...忽略部分输出期望输出中的...表示其余部分忽略对输出较长或含非确定性元素如 PID、时间戳的场景非常实用1 lists:seq(1,100). [1, 2, 3, ...] 2 #{ a b }. #{ a ... } 3 1, 0:1024. 1, 0, 0, 0, ...其原理在 rewrite_tokens/1... 被改写成_/binary , ... ]被改写成| _ ]单独的...被改写成通配变量_使 Shell 风格的省略号变成合法的 Erlang 匹配模式。直接编译完整模块代码块中若出现-module声明ct_doctest会把它当作独立模块源码编译compile:string/1随后可在后续提示符中直接调用-module(my_module). -export([foo/0]). foo() - {?MODULE, ?FUNCTION_NAME, ?LINE}.1 my_module:foo(). {my_module, foo, 4}实现上由 compile_string/3 完成它以模块名构造文件名、调用compile:string/2默认带binary、return_errors和{source, FileName}选项可通过compile_options追加选项成功后用code:load_binary/3加载。模块代码块同样支持预处理特性?MODULE宏、-define宏、-record记录定义这在 ct_doctest_module_preproc_mod.erl 中有完整的三种用例并由 module_preprocessor 测试用例 验证通过。不被解析的边缘情况以下形式的代码块会被解析器忽略不会当作 doctest 运行a should not be tested1 should not be tested should not be testedshould not be tested 1即提示符不是N数字格式、带前导空格、只有而无编号、或首行不是提示符的块都不会被执行。预绑定变量doctest_ok.md 实战解析lib/common_test/test/ct_doctest_SUITE_data/doctest_ok.md是ct_doctest功能自带的测试数据文件内容是一个完整的 Markdown doctest 示例# Markdown doctest erlang 1 Prebound. hello 2 2 3. 5这个示例展示了 doctest 的两大核心能力 1. **多提示符顺序执行**1 与 2 依次递增先求值 Prebound.再求值 2 3. 2. **预绑定变量Prebound Variables**Prebound 变量在文档中并未定义它由测试代码在调用时注入。 该文件在测试套件中正是通过 file/3 配合绑定来运行的[ct_doctest_SUITE.erl 的 file_support 用例](https://link.gitcode.com/i/85764004e45687fe8f3d01ab2388641a) erlang file_support(Config) - DataDir ?config(data_dir, Config), Bindings #{Prebound hello}, ParseErrorFile filename:join(DataDir, doctest_parse_error.md), ok ct_doctest:file(filename:join(DataDir, doctest_ok.md), Bindings, []), ...doctest_ok.md中1 Prebound.的期望输出hello正是Bindings里Prebound hello的结果——文档示例因此可以引用测试运行时才准备好的上下文数据。在模块文档场景中绑定可以按文档条目doc entry细分。Bindings是一个[{KFA | moduledoc, erl_eval:binding_struct()}]列表moduledoc键对应模块文档{function, Name, Arity}以及对应的type/callback键对应具体条目binding_test(_Config) - Bindings [{moduledoc, #{Prebound hello}}], ct_doctest:module(my_module, Bindings, []).绑定值最终作为初始环境传给erl_eval:exprs/2见 run_successful/4并且同一代码块内后续提示符会继承前序提示符产生的新绑定变量可以在提示符间传递。匹配异常测试失败场景文档中经常需要展示会抛异常的例子doctest 同样支持在提示符后写下期望的异常输出即可1 hello 1. ** exception error: an error occurred when evaluating an arithmetic expression in operator /2 called as hello 1 2 lists:last([]). ** exception error: no function clause matching lists:last([])编写技巧最简单的方式是把示例在真实 Shell 中跑一遍、原样复制输出包括** exception行。也可以只写异常消息的开头部分只要与实际异常前缀一致即可通过1 hello 1. ** exception error匹配判定逻辑在 run_failing/4当期望输出以**开头时走失败路径先执行表达式若没有抛出异常则报Expected failure got 结果错误若抛出了异常则用string:prefix/2检查真实异常信息是否以期望文本开头。对应的测试数据可参考 ct_doctest_failure_match_mod.erl测试套件中的 runtime_failure_matching 用例 同时验证了匹配成功意外成功不匹配三种分支。三种运行入口module 与 file 系列 APIct_doctest导出六个函数ct_doctest.erl按调用对象分为模块与文件两组module/1,2,3测试模块的 EEP-48 文档-spec module(Module :: module(), Bindings, Options :: options()) - ok | {comment, string()} | {error, term()} | no_return().module/1等价于module(Module, [])module/2等价于module(Module, [], Options)module/3接受模块名、绑定列表与选项。运行时会检查模块的moduledoc、函数function、类型type和回调callback文档中的所有示例。实现上通过code:get_doc/1读取 EEP-48 文档要求格式为text/markdown除非显式提供了自定义parser选项否则返回{error, unsupported_format}见 module/3 的文档分支。file/1,2,3测试独立 Markdown 文件-spec file(File :: file:filename(), Bindings :: [{atom(), term()}], Options :: options()) - ok | {comment, string()} | {error, term()} | no_return().file/1等价于file(File, [], [])file/2等价于file(File, [], Options)file/3接受文件路径、绑定列表与选项。注意file系列中 Bindings 对所有文件是全局的因此要为每个条目避免命名冲突模块文档中对此有明确提示。如果文件不是 Markdown可以通过自定义parser选项提取其中要测试的代码块。文件读取失败时原样返回{error, Reason}如{error, enoent}。file/3的实现路径ct_doctest.erl是读取文件 →parse/2解析出代码块 →inspect/1→run_blocks/5逐块执行 →ensure_skipped_blocks/2校验跳过块数量 → 返回ok。运行中任何throw:{error, Error}都会打印格式化错误并以error({1, errors})抛出。选项详解options/0ct_doctest支持六种选项类型定义见 ct_doctest.erl 的 options 类型通过proplists传递由 options/1 折叠成内部 record。选项类型默认值说明parserfun((unicode:unicode_binary()) - [unicode:unicode_binary()] \| {error, term()})内置 Markdown 解析器插入外部文档解析器回调必须是一个fun/1返回 Erlang 代码块二进制列表返回的块随后会被检查是否应作为 doctest 运行skipped_blocksnon_neg_integer() \| falsefalse允许被跳过的 Erlang 代码块精确数量因找不到可运行的 Shell 提示符而跳过。不统计missing_tests列出的函数中的块missing_tests[{atom(), arity()}]不检查期望有文档但没有 doctest 的{函数, 元数}列表。设置后新增了无 doctest 的文档函数会失败列表中出现已有 doctest 的函数陈旧条目也会失败skip_tests[moduledoc \| {function\|type\|callback, atom(), arity()}][]跳过指定文档条目的 doctest。例如[moduledoc, {function, foo, 1}]跳过模块文档和foo/1verboseboolean()false打印每个代码块执行与跳过的详细信息compile_options[compile:option()][]编译模块代码块时追加的编译器选项parser自定义文档解析器当文档不是标准 Markdown 时例如格式为text/erlang-shell的自定义格式可提供自定义解析器。仓库中的 ct_doctest_external_parser_mod.erl 给出了一个极简实现——直接把整个内容当做一个代码块返回-module(ct_doctest_external_parser_mod). -moduledoc #{ format ~text/erlang-shell }. -export([parse_doc/1]). %% Custom documentation format -doc 1 1 2.\n3. parse_doc(Content) - [Content].内置解析器parse_markdown_builtin/1ct_doctest.erl会先调用shell_docs_markdown:parse_md/1得到 Markdown AST再通过extract_erlang_code_blocks/1递归遍历 AST只提取class属性包含language-erlang的代码块。自定义 parser 的错误路径在测试中均有覆盖external_parser 用例非函数类型、返回{error, Reason}、返回非列表、列表中混入非二进制元素都会产生对应报错parser 本身抛出异常则原样向上传播。skipped_blocks跳过块计数门禁代码块如果没有可运行的 Shell 提示符比如只有一行普通文本、或只有-module声明会被记为跳过。默认false表示不校验数量设为整数时实际跳过数必须与设定值完全一致否则抛出error({unexpected_skipped_blocks, Expected, Actual})见 ensure_skipped_blocks/2。该行为在 skipped_blocks_option 用例 中通过{skipped_blocks, 0}与期望的error:{unexpected_skipped_blocks, 0, 1}进行了正反验证。missing_tests文档覆盖率护栏这是保证每个文档函数都有 doctest的质量门禁。当设置该选项时ensure_missing_tests/4运行结束会计算三组集合missing有文档但缺少 doctest、且不在期望列表中的函数 → 失败stale在missing_tests中但现在已有 doctest 的函数 → 失败列表过期invalid在missing_tests中但根本没有文档的函数 → 失败列表无效。三者都为空才返回ok否则抛出error({missing_tests_mismatch, [{missing, ...}, {stale, ...}, {invalid, ...}]})。而不设置该选项时缺少测试的函数只会产生{comment, N functions lack tests}提示ensure_missing_tests/2。完整验证逻辑见 missing_tests_option 用例。skip_tests跳过指定条目跳过moduledoc或特定{Kind, Name, Arity}条目的测试。注意若跳过条目在文档中不存在会抛出error({skipped_tests_mismatch, KFA})ensure_skipped_tests/2这能及时发现拼写错误或已删除的函数。验证见 skip_tests_option 用例。verbose逐块调试输出开启后打印每个代码块的运行情况verbose_log/3输出形如ct_doctest(verbose): running block 1 in moduledoc: ... ct_doctest(verbose): passed block 1 in moduledoc ct_doctest(verbose): skipped block 2 in moduledoc (no runnable prompt, 1 skipped): ...在 integration_smoke 用例 中Common Test 甚至用ct_doctest自测自身ct_doctest:module(ct_doctest, [{moduledoc, [{Prebound, hello}]}], [{skipped_blocks, 8}, {verbose, true}])——即对自己 1048 行的模块文档做 doctest并精确断言有 8 个代码块被跳过。返回值与失败行为全部通过返回ok全部通过但有函数缺少测试且未设置missing_tests返回{comment, Comment}有测试失败抛出error({N, errors})N为失败用例数每个失败的详细原因会打印到控制台模块文档格式不支持非text/markdown且无自定义 parser返回{error, unsupported_format}文件读取失败返回{error, Reason}如{error, enoent}。失败信息的格式由 format_error/1 与 format_error_context/1 生成会标明失败位置moduledoc、function f/0、file path与行号。例如测试套件中断言的输出片段A test failed in moduledoc on line 1: 1 ...值不匹配时的消息来自 run_successful/4表达式通过erl_eval:extended_parse_exprs/1解析后执行若抛异常则用 format_exception/4 生成与 Shell 一致的** exception文本并报错。底层执行链路与源码速览一条 doctest 从文档文本到断言完成的完整链路以file/3为例file/3 └─ parse/2内置 Markdown 解析或自定义 parser └─ validate_code_blocks/1 └─ run_blocks/5 └─ test_block/6 └─ run_test/3 ├─ 正则 RE_CAPTURE 识别 N 提示符 或 -module( 声明 ├─ check_prompt_numbers/2编号连续性校验 └─ run_tests/3 ├─ run_successful/4erl_eval:exprs 求值 模式匹配 └─ run_failing/4异常前缀匹配其中两个值得注意的工程细节Shell 风格输出到 Erlang 模式匹配的转换期望输出会被当作匹配模式处理parse_exprs/2中Match \n 1.的小把戏并经过 rewrite_tokens/1...通配、rewrite_match_ast/1Map改:与 maybe_convert_to_literal/1PID/Port/Ref 字面量提升三层改写。错误定位扫描错误与解析错误会携带精确的行号try_parse_exprs/1通过sys_messages:format_messages/4提取带行号的编译器消息因此失败报告能指出文档中的具体行。故障排查Troubleshooting当 doctest 意外失败时按照 ct_doctest.erl 模块文档 的建议依次排查开启verbose打印每个代码块的执行细节快速定位是哪个块、哪个提示符失败核对期望输出期望输出必须与 Shell 输出逐字一致包括空白与换行不确定时先在真实 Shell 中运行并原样复制检查提示符编号与续行缩进编号必须从1递增续行必须缩进首行不能有前导空格%注释只能出现在代码块中不能破坏匹配结构确认代码块语言标注内置 Markdown 解析器只提取language-erlang类的代码块非 Erlang 代码块会被跳过此类块计入skipped_blocks计数。将 doctest 接入日常测试ct_doctest的价值在于把文档维护变成测试的一部分。推荐的落地方式在模块的-doc/-moduledoc属性中按本文格式书写可运行示例在 Common Test 套件中新增一个用例调用ct_doctest:module/1对新写的文档函数用missing_tests选项建立必须有 doctest的护栏防止文档覆盖率回退对输出不稳定的示例PID、端口、大列表使用...通配或忽略结果需要外部上下文的示例用Bindings注入预绑定变量正如 doctest_ok.md 中的Prebound那样。通过这种方式文档示例不再只是给人看的文字而是与代码一同演进、每次测试都真实执行的活测试用例。赞分享编程语言语言运行时标准库编译器并发编程【免费下载链接】otpErlang/OTP项目地址https://gitcode.com/gh_mirrors/ot/otp点击查看免费下载相关推荐Erlang/OTP Common Test 快速上手从第一个测试套件到自动化执行Erlang/OTP Common Test 快速上手从第一个测试套件到自动化执行 本篇技术指南基于 Erlang/OTP 的 Common Test 框架编程语言语言运行时标准库编译器并发编程Common Test 应用全指南Erlang/OTP 自动化测试框架的能力、架构与实战Common Test 应用全指南Erlang/OTP 自动化测试框架的能力、架构与实战 Common Test 是 Erlang/OTP 中用于自动化测试的编程语言语言运行时标准库编译器并发编程Common Test 基础指南Erlang/OTP 自动化测试框架的核心概念与测试套件组织Common Test 基础指南Erlang/OTP 自动化测试框架的核心概念与测试套件组织 Common Test 是 Erlang/OTP 官方集成测试框编程语言语言运行时标准库编译器并发编程创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表