查看源代码 事件处理

概述

Common Test 系统的操作员可以在测试运行期间持续接收事件通知。例如,Common Test 会报告测试用例何时开始和停止,当前成功、失败和跳过的用例计数等等。这些信息可以用于不同的目的,例如以 HTML 以外的格式记录进度和结果,将统计信息保存到数据库以生成报告以及测试系统监控。

Common Test 具有一个基于 OTP 事件管理器概念和 gen_event 行为的事件处理框架。当 Common Test 服务器启动时,它会生成一个事件管理器。在测试执行期间,当发生一些潜在的有趣的事情时,管理器会收到来自服务器的通知。任何插入到事件管理器中的事件处理程序都可以匹配感兴趣的事件,采取行动或传递信息。事件处理程序是 Erlang 模块,由 Common Test 用户根据 gen_event 行为实现(有关详细信息,请参阅模块 gen_event 和系统文档中 OTP 设计原则中的 gen_event 行为 部分)。

Common Test 服务器始终启动一个事件管理器。服务器还会插入一个默认的事件处理程序,其唯一目的是将通知中继到全局注册的 Common Test 主事件管理器(如果在系统中运行 Common Test 主服务器)。Common Test 主服务器也在启动时生成一个事件管理器。插入此管理器的事件处理程序会接收来自所有测试节点的事件,以及来自 Common Test 主服务器的信息。

用户特定的事件处理程序可以插入 Common Test 事件管理器中,可以通过在测试运行之前告知 Common Test 安装它们(稍后描述),也可以在测试运行期间使用 gen_event:add_handler/3gen_event:add_sup_handler/3 动态添加处理程序。在后一种情况下,需要 Common Test 事件管理器的引用。要获取它,请调用 ct:get_event_mgr_ref/0 或(在 Common Test 主节点上)ct_master:get_event_mgr_ref/0

使用

可以通过 event_handler 启动标志 (ct_run) 或选项 ct:run_test/1 安装事件处理程序,其中参数指定一个或多个事件处理程序模块的名称。

示例

$ ct_run -suite test/my_SUITE -event_handler handlers/my_evh1 handlers/my_evh2 -pa $PWD/handlers

要将启动参数传递给事件处理程序的 init 函数,请使用选项 ct_run -event_handler_init 而不是 -event_handler

注意

所有事件处理程序模块都必须具有 gen_event 行为。这些模块必须预编译,并且它们的位置必须显式添加到 Erlang 代码服务器搜索路径中(如上例所示)。

参数 Opts 中的 event_handler 元组具有以下定义(请参阅 ct:run_test/1

{event_handler,EventHandlers}

EventHandlers = EH | [EH]
EH = atom() | {atom(),InitArgs} | {[atom()],InitArgs}
InitArgs = [term()]

在以下示例中,为 my_SUITE 测试安装了两个事件处理程序

1> ct:run_test([{suite,"test/my_SUITE"},{event_handler,[my_evh1,{my_evh2,[node()]}]}]).

事件处理程序 my_evh1 使用 [] 作为 init 函数的参数启动。事件处理程序 my_evh2 使用当前节点的名称作为 init 参数列表启动。

还可以使用以下 测试规范 项之一插入事件处理程序

  • {event_handler, EventHandlers}
  • {event_handler, EventHandlers, InitArgs}
  • {event_handler, NodeRefs, EventHandlers}
  • {event_handler, NodeRefs, EventHandlers, InitArgs}

EventHandlers 是模块名称的列表。在测试会话开始之前,会调用每个插入的事件处理程序的 init 函数(以 InitArgs 列表作为参数,如果未指定启动参数,则为 [])。

要将处理程序插入 Common Test 主事件管理器,请在 NodeRefs 中指定 master 作为节点。

为了能够匹配事件,事件处理程序模块必须包含头文件 ct_event.hrl。事件是具有以下定义的记录

#event{name, node, data}

  • name - 事件的标签(类型)。

  • node - 事件来源的节点名称(仅与 Common Test 主事件处理程序相关)。

  • data - 特定于事件。

通用事件

通用事件如下

  • #event{name = start_logging, data = LogDir} - LogDir = string(),测试运行的顶层日志目录。

    此事件表示 Common Test 的日志记录过程已成功启动并准备好接收 I/O 消息。

  • #event{name = stop_logging, data = []} - 此事件表示 Common Test 的日志记录过程在测试运行结束时已关闭。

  • #event{name = test_start, data = {StartTime,LogDir}} - StartTime = {date(),time()},测试运行开始日期和时间。

    LogDir = string(),测试运行的顶层日志目录。

    此事件表示 Common Test 已完成初始准备并开始执行测试用例。

  • #event{name = test_done, data = EndTime} - EndTime = {date(),time()},测试运行结束的日期和时间。

    此事件表示最后一个测试用例已执行,并且 Common Test 正在关闭。

  • #event{name = start_info, data = {Tests,Suites,Cases}} - Tests = integer(),测试次数。

    Suites = integer(),套件总数。

    Cases = integer() | unknown,测试用例总数。

    此事件提供初始测试运行信息,可以解释为:“此测试运行将执行 Tests 个单独的测试,总共包含 Cases 个测试用例,在 Suites 个套件中”。但是,如果任何测试中存在具有重复属性的测试用例组,则无法计算测试用例总数(未知)。

  • #event{name = tc_start, data = {Suite,FuncOrGroup}} - Suite = atom(),测试套件的名称。

    FuncOrGroup = Func | {Conf,GroupName,GroupProperties}

    Func = atom(),测试用例或配置函数的名称。

    Conf = init_per_group | end_per_group,组配置函数。

    GroupName = atom(),组的名称。

    GroupProperties = list(),组的执行属性列表。

    此事件通知测试用例或组配置函数的开始。该事件也会为 init_per_suiteend_per_suite 发送,但不会为 init_per_testcaseend_per_testcase 发送。如果组配置函数启动,还会指定组名称和执行属性。

  • #event{name = tc_logfile, data = {{Suite,Func},LogFileName}} - Suite = atom(),测试套件的名称。

    Func = atom(),测试用例或配置函数的名称。

    LogFileName = string(),测试用例日志文件的完整名称。

    此事件在每个测试用例(和配置函数,除了 init/end_per_testcase)开始时发送,并携带有关当前测试用例日志文件的完整名称(即,包括绝对目录路径的文件名)的信息。

  • #event{name = tc_done, data = {Suite,FuncOrGroup,Result}} - Suite = atom(),套件的名称。

    FuncOrGroup = Func | {Conf,GroupName,GroupProperties}

    Func = atom(),测试用例或配置函数的名称。

    Conf = init_per_group | end_per_group,组配置函数。

    GroupName = unknown | atom(),组的名称(如果 init 或 end 函数超时,则未知)。

    GroupProperties = list(),组的执行属性列表。

    Result = ok | {auto_skipped,SkipReason} | {skipped,SkipReason} | {failed,FailReason},结果。

    SkipReason = {require_failed,RequireInfo} | {require_failed_in_suite0,RequireInfo} | {failed,{Suite,init_per_testcase,FailInfo}} | UserTerm,为什么跳过用例。

    FailReason = {error,FailInfo} | {error,{RunTimeError,StackTrace}} | {timetrap_timeout,integer()} | {failed,{Suite,end_per_testcase,FailInfo}},失败的原因。

    RequireInfo = {not_available,atom() | tuple()},为什么 require 失败。

    FailInfo = {timetrap_timeout,integer()} | {RunTimeError,StackTrace} | UserTerm,错误详细信息。

    RunTimeError = term(),运行时错误,例如 badmatchundef

    StackTrace = list(),运行时错误之前的函数调用列表。

    UserTerm = term(),用户指定的任何数据,或 exit/1 信息。

    此事件通知测试用例或配置函数的结束(有关元素 FuncOrGroup 的详细信息,请参阅事件 tc_start)。此事件带有相关函数的最终结果。可以在 Result 的顶层确定该函数是否成功、跳过(由用户)或失败。

    还可以深入挖掘,例如,对跳过或失败的各种原因执行模式匹配。请注意,{'EXIT',Reason} 元组转换为 {error,Reason}。另请注意,如果收到 {failed,{Suite,end_per_testcase,FailInfo} 结果,则表示测试用例已成功,但该用例的 end_per_testcase 失败。

  • #event{name = tc_auto_skip, data = {Suite,TestName,Reason}} - Suite = atom(),套件的名称。

    TestName = init_per_suite | end_per_suite | {init_per_group,GroupName} | {end_per_group,GroupName} | {FuncName,GroupName} | FuncName

    FuncName = atom(),测试用例或配置函数的名称。

    GroupName = atom(),测试用例组的名称。

    Reason = {failed,FailReason} | {require_failed_in_suite0,RequireInfo},自动跳过 Func 的原因。

    FailReason = {Suite,ConfigFunc,FailInfo}} | {Suite,FailedCaseInSequence},失败的原因。

    RequireInfo = {not_available,atom() | tuple()},为什么 require 失败。

    ConfigFunc = init_per_suite | init_per_group

    FailInfo = {timetrap_timeout,integer()} | {RunTimeError,StackTrace} | bad_return | UserTerm,错误详细信息。

    FailedCaseInSequence = atom(),序列中失败的用例的名称。

    RunTimeError = term(),运行时错误,例如 badmatchundef

    StackTrace = list(),运行时错误之前的函数调用列表。

    UserTerm = term(),用户指定的任何数据,或 exit/1 信息。

    Common Test由于以下原因自动跳过每个测试用例或配置函数时,会发送此事件:init_per_suiteinit_per_group 失败,suite/0 中的 require 失败,或序列中的测试用例失败。请注意,由于 init_per_testcase 失败而导致测试用例被跳过,绝不会收到此事件,因为该信息会在事件 tc_done 中携带。如果失败的测试用例属于测试用例组,则第二个数据元素是元组 {FuncName,GroupName},否则仅是函数名称。

  • #event{name = tc_user_skip, data = {Suite,TestName,Comment}} - Suite = atom(),套件的名称。

    TestName = init_per_suite | end_per_suite | {init_per_group,GroupName} | {end_per_group,GroupName} | {FuncName,GroupName} | FuncName

    FuncName = atom(),测试用例或配置函数的名称。

    GroupName = atom(),测试用例组的名称。

    Comment = string(),测试用例被跳过的原因。

    此事件指定测试用例被用户跳过。仅当在测试规范中声明跳过时才会收到此事件。否则,用户跳过信息会作为事件 tc_done 中测试用例的 {skipped,SkipReason} 结果接收。如果跳过的测试用例属于测试用例组,则第二个数据元素是元组 {FuncName,GroupName},否则仅是函数名称。

  • #event{name = test_stats, data = {Ok,Failed,Skipped}} - Ok = integer(),当前成功的测试用例数。

    Failed = integer(),当前失败的测试用例数。

    Skipped = {UserSkipped,AutoSkipped}

    UserSkipped = integer(),当前用户跳过的测试用例数。

    AutoSkipped = integer(),当前自动跳过的测试用例数。

    这是一个统计事件,其中包含到目前为止成功、跳过和失败的测试用例的当前计数。此事件在每个测试用例结束后发送,紧随事件 tc_done 之后。

内部事件

内部事件如下:

  • #event{name = start_make, data = Dir} - Dir = string(),在此目录中运行 make。

    此内部事件表示 Common Test 开始在目录 Dir 中编译模块。

  • #event{name = finished_make, data = Dir} - Dir = string(),完成在此目录中运行 make。

    此内部事件表示 Common Test 已完成在目录 Dir 中编译模块。

  • #event{name = start_write_file, data = FullNameFile} - FullNameFile = string(),文件的完整名称。

    此内部事件由 Common Test 主进程用于同步特定的文件操作。

  • #event{name = finished_write_file, data = FullNameFile} - FullNameFile = string(),文件的完整名称。

    此内部事件由 Common Test 主进程用于同步特定的文件操作。

说明

事件也在 ct_event.erl 中进行了文档化。该模块可以作为 Common Test 事件管理器事件处理程序的示例。

注意

为了确保打印到 stdout 的内容(或使用 ct:log/2,3ct:pal,2,3 进行的打印)被写入到测试用例日志文件,而不是 Common Test 框架日志,您可以通过匹配事件 tc_starttc_done 来与 Common Test 服务器进行同步。在这些事件之间的时间段内,所有 I/O 都将定向到测试用例日志文件。这些事件是同步发送的,以避免潜在的时序问题(例如,在外部进程的 I/O 消息通过之前,测试用例日志文件被关闭)。了解这一点后,您需要小心,您的 handle_event/2 回调函数不会使测试执行停滞,从而可能导致意外行为。