编程语言语言运行时标准库编译器并发编程【免费下载链接】otpErlang/OTP项目地址https://gitcode.com/gh_mirrors/ot/otp点击查看免费下载本文基于 Common Test Hooks 编写并结合 ct_hooks.erl、cth_log_redirect.erl、cth_surefire.erl 等源码与测试用例进行深化。导读Common Test HookCTH是 Erlang/OTPcommon_test应用提供的一套回调框架允许在测试套件suite的所有配置函数调用前后注入自定义逻辑从而把多个测试套件共有的行为日志、外部系统启停监控、编译测试所需 C 文件等抽象出来避免在每个套件里重复编写样板代码。读完本文你将掌握 CTH 的三种全局安装方式、套件内安装方法、基于id/1的去重与覆盖机制、ct_hooks_order优先级调度模型、作用域scope生命周期以及通过 pre/post 钩子操控测试结果的完整方案。CTH 是什么一段插在 Common Test 与测试套件之间的中间件从源码看ct_hooks模块被定义为 A callback interface on top of Common Testct_hooks.erl其核心价值可以浓缩为两点操控运行时配置在每个套件配置调用之前可以改写将要传给配置函数的配置列表Config。操控所有配置调用的返回值进而间接影响测试本身的执行结果如把失败恢复为通过、把失败改为跳过等。CTH 适合高级用户使用——它的典型应用场景包括日志记录、启动与监控外部系统、为测试编译 C 文件、数据库一致性检查、JUnit/XML 报告生成等。它的本质是把多个套件共有的横切逻辑抽象出来一次编写、处处生效与测试套件本身解耦。警告来自官方文档在 CTH 内部执行时所有 timetrap超时陷阱都被关闭。如果你的 CTH 永远不返回整个测试运行将被卡死。因此 CTH 回调必须保证能返回不能依赖 timetrap 兜底。安装 CTH全局与套件内两种途径全局安装三种方式如果希望某个 CTH 在整次测试运行的所有套件中都生效官方文档给出了三种等价方式ct_run命令行参数-ct_hooks多个 CTH 用关键字and拼接例如ct_run -ct_hooks cth1 [{debug,true}] and cth2 ...每个 CTH 后可跟其初始化参数proplist。Test Specification 中的ct_hooks标签在测试规范文件中声明适用于需要批量、可复现的测试运行配置详见 run_test_chapter.md。ct:run_test/1参数中的ct_hooks标签以编程方式启动测试时传入。套件内安装CTH 也可以在测试套件内部动态安装——从suite/0、init_per_suite/1或init_per_group/2返回的配置列表中携带{ct_hooks,[CTH]}项即可分别对应ct_suite行为中的这三个信息函数。CTH可以是以下三种形态之一{ct_hooks,[my_cth_module]} %% 仅模块名 {ct_hooks,[{my_cth_module,[{debug,true}]}]} %% 模块名 初始参数 {ct_hooks,[{my_cth_module,[{debug,true}],500}]}%% 模块名 参数 优先级源码 ct_hooks.erl 的get_new_hooks/2正是按照这三种形态解析配置列表中的{ct_hooks, ...}项的原子直接作为模块、二元组解析为{Module, Opts}、三元组解析为{Module, Opts, Prio}并统一包装为#ct_hook_config{}记录。前提条件无论以哪种方式安装CTH 的 BEAM 文件必须位于代码路径code path中。ct_run接受-pa选项来追加代码路径例如ct_run -suite example_SUITE -pa . -ct_hooks example_cth。用id/1去重与覆盖 CTH默认情况下每次安装 CTH 都会激活一个新实例。这在测试规范里已经装了 CTH、套件信息函数里又装了一遍的场景下会导致重复执行。ct_hooks:id/1回调正是为解决此问题而设计只要在两端返回相同的IdCommon Test 就认为该 CTH 已安装不会重复实例化。从实现上看call/4在处理call_id时会通过lists:keyfind(NewId, #ct_hook_config.id, Hooks)检查已安装钩子列表若Id已存在则跳过安装ct_hooks.erl。官方文档特别强调id/1不允许有任何副作用因为它可能被 Common Test 多次调用如果未实现默认行为等价于返回make_ref()产生的新引用即每次都视为新实例。CTH 执行顺序安装序、优先级与ct_hooks_order默认顺序测试中心test-centric默认情况下各 CTH 按安装顺序执行 init 类回调而 end 类回调则按安装顺序的逆序执行——因为这是围绕一个测试用例执行完毕展开的对称收尾官方称之为test-centric测试中心对应ct_hooks_order选项的默认值test。源码中的resort/4函数ct_hooks.erl明确列出了在test模式下需要逆序执行的钩子集合[pre_end_per_testcase, post_end_per_testcase, pre_end_per_group, post_end_per_group, pre_end_per_suite, post_end_per_suite]即所有*_end_per_*类的 pre/post 钩子都会被反转顺序。用优先级Priority控制顺序安装序并非总是符合预期因此 Common Test 允许为每个钩子指定优先级。优先级可以在两个位置声明CTH 的init/2回调返回{ok, State, Priority}安装时在元组中显式给出如{my_cth_module,[{debug,true}],500}。安装时指定的优先级会覆盖init/2返回的优先级。这一逻辑体现在call_init/3中只有当安装时未指定优先级P : undefined时才会采用init/2返回的Prioct_hooks.erl。优先级数值越小越先执行未指定时默认为0。排序实现在resort/2中ct_hooks.erl除了普通整型优先级外还存在两个特殊优先级ctfirst和ctlast——ctfirst永远排在最前、ctlast永远排在最后内置钩子cth_log_redirect即以ctfirst优先级注册见?BUILTIN_HOOKS宏定义。当优先级相同时按安装先后位置pos/3排序。ct_hooks_SUITE.erl中的prio_cth测试用例即验证了多优先级安装下的执行序列见 ct_hooks_SUITE.erl。配置中心config-centric模式在某些场景下所有 end 调用统一逆序并不理想用户可能更希望post 钩子按逆序执行。将ct_hooks_order选项设为config即可启用configuration-centric配置中心模式逆序发生在每个配置函数之后而不是相对于测试用例。源码中resort/4对config模式列出的逆序钩子集合为[post_init_per_testcase, post_end_per_testcase, post_init_per_group, post_end_per_group, post_init_per_suite, post_end_per_suite]注意ct_hooks_order被视为全局框架设置如果该选项被配置了多次框架只处理第一个值见 ct_hooks.erl 中process_hooks_order/2的首次保存、后续忽略逻辑。它同样可以通过三种途径设置ct_run参数、测试规范或suite/0的返回值。ct_hooks_SUITE_data下的ct_hooks_order_config_suite_SUITE.erl、ct_hooks_order_config_ips_SUITE.erl、ct_hooks_order_test_SUITE.erl等即为这两种模式的专项测试套件。CTH 作用域Scope从哪里开始到哪里结束CTH 一旦安装就会一直存在直到其作用域结束。作用域取决于安装时机init/2在作用域开始时被调用terminate/1在作用域结束时被调用。官方文档给出的作用域对照表如下CTH 安装位置作用域开始于作用域结束于ct_run第一个测试套件运行之前最后一个测试套件运行之后ct:run_test/1第一个测试套件运行时最后一个测试套件运行之后Test Specification第一个测试套件运行时最后一个测试套件运行之后suite/0调用pre_init_per_suite/3时该测试套件调用完post_end_per_suite/4之后init_per_suite/1调用post_init_per_suite/4时该测试套件调用完post_end_per_suite/4之后init_per_group/2调用post_init_per_group/5时该组调用完post_end_per_group/5之后源码中的scope/1函数ct_hooks.erl负责把 pre/post 钩子点翻译成对应的作用域终止点例如pre_init_per_testcase→post_init_per_testcase、pre_init_per_suite→post_end_per_suite等而terminate_if_scope_ends/3ct_hooks.erl则在实际执行到作用域终点时触发terminate/1并从钩子列表中移除该实例。进程与 ETS 表注意事项CTH 与普通测试套件遵循相同的进程作用域执行init_per_suite钩子与执行init_per_group/per_testcase 钩子的是不同的进程。因此如果你在 CTH 里 spawn 进程不能与 CTH 进程 link——因为 CTH 进程在 post 钩子结束后即退出如果你需要 ETS 表必须单独 spawn 一个进程来持有它ETS 表归属创建它的进程。外部配置数据与日志CTH 内可以通过ct:get_config/1,2,3读取配置数据前提是这些配置变量已由套件/组/测试用例信息函数或ct:require/1/2声明过 requiredct:require/1也可直接在 CTH 函数中使用详见 config_file_chapter.md 的 Requiring and Reading Configuration Data 一节。此外CTH 回调函数可以调用ct接口中的任何日志函数把信息打印到日志文件或在套件概览页suite overview中添加注释。操控测试Pre、Post 与 Skip/Fail 钩子通过 CTH 可以操控测试与配置函数的结果目的就是让通用模式从套件中抽离出来、在多个套件间复用而无需复制代码。所有 CTH 回调遵循统一接口。Common Test 总是调用所有可用的钩子函数——即使套件没有实现某个配置函数对应的 pre/post 钩子也会被调用。例如对x_SUITE即使它没有导出init_per_suite/1pre_init_per_suite(x_SUITE, ...)和post_init_per_suite(x_SUITE, ...)依然会被调用。利用这一点钩子可以作为配置回退fallback甚至用钩子函数替换掉全部配置函数。Pre 钩子在配置调用之前介入可以在以下 6 个函数之前挂接行为init_per_suite/1init_per_group/2init_per_testcase/2end_per_testcase/2end_per_group/2end_per_suite/1对应 CTH 函数命名为pre_函数名参数为SuiteName、Name如适用组名或测试用例名、Config、CTHState。返回值总是结果 更新后的CTHState二元组。要让测试继续执行就把希望测试使用的配置列表作为结果返回。除pre_end_per_testcase/4外所有 pre 钩子都可以通过返回{skip, Reason}或{fail, Reason}元组跳过或使测试失败。官方示例pre_init_per_suite(SuiteName, Config, CTHState) - case db:connect() of {error,_Reason} - {{fail, Could not connect to DB}, CTHState}; {ok, Handle} - {[{db_handle, Handle} | Config], CTHState#state{ handle Handle }} end.多 CTH 协作提示如果使用多个 CTH返回元组的第一部分会作为下一个 CTH 的输入。因此上例中下一个 CTH 可能收到{fail,Reason}作为第二个参数。若多个 CTH 互相联动不要让每个 CTH 都返回fail或skip而应通过Config列表传递需要采取动作的信号并由最后一个 CTH 统一执行正确动作。Post 钩子在配置调用之后介入可以在同样 6 个函数之后挂接行为对应 CTH 函数命名为post_函数名参数为SuiteName、Name、Config、Return、CTHState。这里的Config与测试用例收到的Config相同Return是测试用例的返回值。若测试用例崩溃失败Return为{EXIT,{{Error,Reason},Stacktrace}}。返回值同样是结果 新 CTHState。如果不想影响测试结果就原样返回Return也可以修改测试结果——通过返回删除了tc_status元素的Config列表来从失败中恢复与 pre 钩子一样post 钩子中也可以返回{fail, ...}/{skip, ...}使测试失败/跳过。官方示例post_end_per_testcase(_Suite, _TC, Config, {EXIT,{_,_}}, CTHState) - case db:check_consistency() of true - %% DB is good, pass the test. {proplists:delete(tc_status, Config), CTHState}; false - %% DB is not good, mark as skipped instead of failing {{skip, DB is inconsistent!}, CTHState} end; post_end_per_testcase(_Suite, _TC, Config, Return, CTHState) - %% Do nothing if tc does not crash. {Return, CTHState}.官方忠告通过 CTH 从测试失败中恢复只应作为最后手段。若使用不当会很难判断一次测试运行中到底哪些测试通过、哪些失败。从源码看post 钩子由end_tc/5统一派发ct_hooks.erl例如post_init_per_suite把init_per_suite的返回作为Value传入、post_end_per_testcase把测试用例结果传入回调未导出时还会通过call_generic_fallback尝试调用旧式参数更少的回调签名以保持向后兼容catch_apply/5的 Fallback 机制ct_hooks.erl。Skip 和 Fail 钩子结果已成定局当所有已安装 CTH 的 post 钩子执行完毕后若测试用例失败或被跳过Common Test 会分别调用on_tc_fail/4或on_tc_skip/4。此时已无法再影响测试结果只能做记录/统计等收尾工作。on_tc_skip/4与on_tc_fail/4的语义差异、以及它们在配置函数跳过/失败场景下的调用时机详见 ct_hooks.erl 中的回调文档如init_per_group被跳过时在post_init_per_group之后调用。若on_tc_skip/4/on_tc_fail/4未导出框架会退回调用参数更少的旧式on_tc_skip/3/on_tc_fail/3。钩子中的ct:comment/1-2限制从某些进程调用ct:comment/1-2存在限制详见ct:comment/1文档。此外当从钩子的init/2和terminate/1中调用时其行为取决于钩子的作用域若钩子的作用域开始于pre_init_per_suite/3被调用之前即通过ct_run/ct:run_test/1/测试规范安装那么从运行init/2或terminate/1的进程调用ct:comment/1-2注释不会打印到任何地方否则注释会打印在与init_per_suite/end_per_suite函数进程相同的位置——即生成的 Common Test HTML 日志中对应的 init_per_suite / end_per_suite 行。与外部用户应用同步挂起 Common TestCTH 还可以用来把测试运行与外部用户应用同步init函数可以启动/通信某个用于准备被测系统SUT的应用或初始化用于保存测试数据的数据库terminate函数可以命令该应用在测试后重置 SUT、结束活动会话并退出init/terminate 阶段产生的系统错误报告或进度报告会被保存到 Pre- and Post Test I/O Log参见 run_test_chapter.mdct:log/2与ct:pal/2的打印也是如此。为保证 Common Test 不会在外部应用就绪前就开始执行测试或过早关闭日志文件/关机可以让 CTH 在 init 或 terminate 函数中执行一个receive表达式来挂起 Common Test直到收到外部应用的完成消息。ct.hrl中定义了两个宏用于指明应该向哪个 Common Test 进程发送消息以解除receive?CT_HOOK_INIT_PROCESS—— 执行钩子 init 函数的进程?CT_HOOK_TERMINATE_PROCESS—— 执行钩子 terminate 函数的进程。在 lib/common_test/include/ct.hrl 中两者目前都定义为ct_util_server。用法示意init(Id, Opts) - %% 等待外部应用就绪 receive {external_app_ready, Pid} - ok after 5000 - ok end, {ok, State}.完整示例一个把测试运行信息写入文件的 CTH官方文档提供了一个完整可运行的示例example_cth它把一次测试运行的信息总用例数、跳过数、每个套件的用例数与耗时等以file:consult/1可解析的格式写入文件。其命令行用法为ct_run -suite example_SUITE -pa . -ct_hooks example_cth注意-pa .钩子的 beam 文件在安装时必须位于代码路径中。%%% Common Test Example Common Test Hook module. %%% %%% To use this hook, on the command line: %%% ct_run -suite example_SUITE -pa . -ct_hooks example_cth %%% %%% Note -pa .: the hook beam file must be in the code path when installing. -module(example_cth). %% Mandatory Callbacks -export([init/2]). %% Optional Callbacks -export([id/1]). -export([pre_init_per_suite/3]). -export([post_end_per_suite/4]). -export([pre_init_per_testcase/4]). -export([post_end_per_testcase/5]). -export([on_tc_skip/4]). -export([terminate/1]). %% This hook state is threaded through all the callbacks. -record(state, {filename, total, suite_total, ts, tcs, data, skipped}). %% This example hook prints its results to a file, see terminate/1. -record(test_run, {total, skipped, suites}). %% Return a unique id for this CTH. %% Using the filename means the hook can be used with different %% log files to separate timing data within the same test run. %% See Installing a CTH for more information. id(Opts) - %% the path is relative to the test run directory proplists:get_value(filename, Opts, example_cth.log). %% Always called before any other callback function. Use this to initiate %% any common state. init(Id, _Opts) - {ok, #state{filename Id, total 0, data []}}. %% Called before init_per_suite is called. pre_init_per_suite(_Suite,Config,State) - {Config, State#state{suite_total 0, tcs []}}. %% Called after end_per_suite. post_end_per_suite(Suite,_Config,Return,State) - Data {suites, Suite, State#state.suite_total, lists:reverse(State#state.tcs)}, {Return, State#state{data [Data | State#state.data], total State#state.total State#state.suite_total}}. %% Called before each init_per_testcase. pre_init_per_testcase(_Suite,_TC,Config,State) - Now erlang:monotonic_time(microsecond), {Config, State#state{ts Now, suite_total State#state.suite_total 1}}. %% Called after each end_per_testcase. post_end_per_testcase(Suite,TC,_Config,Return,State) - Now erlang:monotonic_time(microsecond), TCInfo {testcase, Suite, TC, Return, Now - State#state.ts}, {Return, State#state{ts undefined, tcs [TCInfo | State#state.tcs]}}. %% Called when a test case is skipped by either user action %% or due to an init function failing. on_tc_skip(_Suite, _TC, _Reason, State) - State#state{skipped State#state.skipped 1}. %% Called when the scope of the CTH is done. terminate(State) - %% use append to avoid data loss if the path is reused {ok, File} file:open(State#state.filename, [write, append]), io:format(File, ~p.~n, [results(State)]), file:close(File), ok. results(State) - #state{skipped Skipped, data Data, total Total} State, #test_run{total Total, skipped Skipped, suites lists:reverse(Data)}.这个例子展示了 CTH 的全部关键约定init/2是唯一强制回调在ct_hooks行为的-optional_callbacks列表中init/2不在其列其余均为可选见 ct_hooks.erl状态穿针引线#state{}记录被贯穿所有回调每次调用返回{结果, NewState}id/1返回基于文件名的 Id同一钩子可用不同日志文件在同一测试运行中隔离计时数据且能避免重复安装terminate/1负责资源收尾以 append 模式打开文件防止路径复用导致数据丢失。需要提醒的是post_all/3与post_groups/2这两个用于修改测试集合/组定义的回调从all/0、groups/0返回值之后调用——官方文档与源码注释均明确指出每次测试执行前 Common Test 都会做一次模拟运行以统计套件、组与用例数量用于日志因此这两个回调总是被调用两次应避免在其中产生副作用ct_hooks.erl。内置 CTH开箱即用的通用测试功能common_test随附若干通用 CTH其中部分在common_test启动运行时默认启用。可通过在命令行或测试规范中设置enable_builtin_hooks为false来禁用。从源码看内置钩子定义于?BUILTIN_HOOKS宏ct_hooks.erl目前默认内置的只有一个cth_log_redirect优先级ctfirst并由get_builtin_hooks/1根据enable_builtin_hooks选项决定是否装载。cth_log_redirect内置捕获通常由默认 logger handler 打印的所有日志事件并把它们输出到当前测试用例日志如果某事件无法关联到具体测试用例如并行测试用例之间的事件则打印到 Common Test 框架日志。其实现位于 cth_log_redirect.erl它实现为 logger handler 加 gen_server在init/2中启动日志 handlerstart_log_handler(Opts)并在post_end_per_testcase/5中通过gen_server:call(?MODULE, flush, 300000)确保事件队列在用例结束前被冲刷。格式与级别在 CTH 启动时从当前的defaulthandler 复制若想使用其他级别可在启动 common_test 前修改defaulthandler 级别或使用logger:set_handler_config/3API。支持以下选项{mode, add}默认行为把cth_log_redirect添加到默认日志 handler 之上日志既输出到标准输出经默认 handler也进入 Common Test HTML 日志{mode, replace}用cth_log_redirect替换defaulthandler从而静默测试运行期间原本会打印到标准输出的 logger 输出。启用方式ct_run -enable_builtin_hooks false -ct_hooks cth_log_redirect [{mode,replace}]cth_surefire非内置捕获所有测试结果并输出为 surefire XML 文件默认文件名为junit_report.xml。可通过path选项修改文件名-ct_hooks cth_surefire [{path,/tmp/report.xml}]若设置url_base选项每个testsuite和testcaseXML 元素会额外增加名为url的属性其值由url_base与该套件/用例日志的相对路径拼接而成-ct_hooks cth_surefire [{url_base, http://myserver.com/}]生成效果类似http://myserver.com/ct_run.ctmyhost.2012-12-12_11.19.39/x86_64-unknown-linux-gnu.my_test.logs/run.2012-12-12_11.19.39/suite.log.htmlSurefire XML 可用于 Jenkins 等 CI 系统展示测试结果。从 cth_surefire.erl 源码看它有几点值得一提的实现细节id/1基于path选项生成默认junit_report.xml的绝对路径因此不同path对应不同实例可同时生成多份报告采用gen_server 代理proxy设计init/2启动一个独立进程持有状态避免大测试套件如diameter_traffic_SUITE把巨大的状态复制进每个用例进程——注释中明确记录该优化把该套件执行时间从 30 分钟降到 5 分钟通过beam_lib:chunks(code:which(Suite),[debug_info])读取套件的调试信息以获得测试用例的源码位置file/line用于 XML 输出。深入ct_hooks行为的完整回调清单与兼容性最后把 ct_hooks.erl 中定义的完整回调接口汇总如下便于编写时对照强制回调init/2返回{ok, State}或{ok, State, Priority}。可选回调-optional_callbacks列表id/1、on_tc_fail/4、on_tc_skip/4、post_all/3、post_end_per_group/5、post_end_per_suite/4、post_end_per_testcase/5、post_groups/2、post_init_per_group/5、post_init_per_suite/4、post_init_per_testcase/5、pre_end_per_group/4、pre_end_per_suite/3、pre_end_per_testcase/4、pre_init_per_group/4、pre_init_per_suite/3、pre_init_per_testcase/4、terminate/1。几个值得注意的兼容性事实均可在 ct_hooks.erl 的回调文档中确认多个回调存在旧式签名回退如post_end_per_testcase/5未导出时尝试调用post_end_per_testcase(TestcaseName, Config, Return, CTHState)少一个 SuiteNamepre_end_per_testcase/4未导出时尝试pre_end_per_testcase(TestcaseName, EndData, CTHState)。catch_apply/5的 Fallback 机制统一处理这类回退pre_end_per_testcase/4不能通过返回 skip/fail 元组改变测试结果但可以往Config中插入end_per_testcase/2或post_end_per_testcase/5可读取的条目pre_init_per_testcase/4目前不允许添加新 CTH源码注释明确说明该特性可能在未来版本加入但当前会破坏向后兼容并行组parallel group执行时多个进程会同时读写同一 CTH 的状态框架为此在 ct_hooks.erl 中提供了ct_hooks_lock锁机制maybe_lock/0、maybe_unlock/0只在并行组的 pre/post 阶段加锁保护状态一致性CTH 回调若抛出异常会被catch_apply/3捕获并记录到 Suite Hook 日志然后以{fail, Reason}结果继续ct_hooks.erl。参考资料本章节原文lib/common_test/doc/guides/ct_hooks_chapter.mdCTH 行为定义与实现lib/common_test/src/ct_hooks.erl内置日志重定向 CTHlib/common_test/src/cth_log_redirect.erl内置 Surefire XML CTHlib/common_test/src/cth_surefire.erl宏CT_HOOK_INIT_PROCESS/CT_HOOK_TERMINATE_PROCESSlib/common_test/include/ct.hrl测试套件与数据lib/common_test/test/ct_hooks_SUITE.erl、lib/common_test/test/ct_surefire_SUITE.erl相关指南运行测试与测试规范、配置数据读取、事件处理赞分享编程语言语言运行时标准库编译器并发编程【免费下载链接】otpErlang/OTP项目地址https://gitcode.com/gh_mirrors/ot/otp点击查看免费下载相关推荐Erlang/OTP测试框架终极指南掌握EUnit和Common TestErlang/OTP测试框架终极指南掌握EUnit和Common Test Erlang/OTP是构建高并发、高可靠性系统的强大平台而EUnit和Commo编程语言语言运行时标准库编译器并发编程Erlang/OTP Common Test 安装指南ct_run 可执行程序与 ct 模块的双接口体系Erlang/OTP Common Test 安装指南ct_run 可执行程序与 ct 模块的双接口体系 Common Test 是 Erlang/OTP 内编程语言语言运行时标准库编译器并发编程Common Test 应用全指南Erlang/OTP 自动化测试框架的能力、架构与实战Common Test 应用全指南Erlang/OTP 自动化测试框架的能力、架构与实战 Common Test 是 Erlang/OTP 中用于自动化测试的编程语言语言运行时标准库编译器并发编程创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考