查看源码 外部配置数据

概述

为了避免在测试套件中硬编码与测试和/或被测系统 (SUT) 相关的数据值,可以改为通过配置文件或字符串来指定数据,Common Test 在测试运行开始之前读取这些数据。外部配置数据使得无需修改使用这些数据的测试套件即可更改测试属性。配置数据的示例如下:

  • 测试设备或其他仪器的地址
  • 用户登录信息
  • 测试所需的文件名称
  • 测试期间要执行的程序名称
  • 测试所需的任何其他变量

语法

一个配置文件可以包含任意数量的以下类型的元素:

{CfgVarName,Value}.

其中:

CfgVarName = atom()
Value = term() | [{CfgVarName,Value}]

要求和读取配置数据

在测试套件中,必须在尝试读取测试用例或配置函数中的关联值之前要求配置变量(前面定义中的 CfgVarName)存在。

require 是一个断言语句,它可以是 测试套件信息函数测试用例信息函数 的一部分。如果所需的变量不可用,则会跳过测试(除非已指定默认值,有关详细信息,请参阅 测试用例信息函数 部分)。此外,可以从测试用例调用函数 ct:require/1/2 以检查特定变量是否可用。必须显式检查此函数的返回值,并根据结果采取适当的操作(例如,如果相关变量不存在,则跳过测试用例)。

测试套件信息情况或测试用例信息列表中的 require 语句看起来像 {require,CfgVarName}{require,AliasName,CfgVarName}。参数 AliasNameCfgVarNamect:require/1,2 的参数相同。AliasName 成为配置变量的别名,可以用作对配置数据值的引用。配置变量可以与任意数量的别名关联,但每个名称在同一测试套件中必须是唯一的。别名的两个主要用途如下:

  • 识别连接(稍后描述)。
  • 帮助将配置数据调整到测试套件(或测试用例)并提高可读性。

要读取配置变量的值,请使用函数 get_config/1,2,3

示例

suite() ->
    [{require, domain, 'CONN_SPEC_DNS_SUFFIX'}].

...

testcase(Config) ->
    Domain = ct:get_config(domain),
    ...

使用在多个文件中定义的配置变量

如果一个配置变量在多个文件中定义,并且您想要访问所有可能的值,请使用函数 ct:get_config/3 并在选项列表中指定 all。然后,这些值将以列表的形式返回,元素的顺序与启动时指定的配置文件的顺序相对应。

加密的配置文件

如果包含敏感数据的配置文件必须存储在开放和共享的目录中,则可以对其进行加密。

要让 Common Test 使用应用程序 Crypto 中的函数 DES3 加密指定的文件,请调用 ct:encrypt_config_file/2,3。然后,加密文件可以与其他加密文件或普通文本文件组合使用,作为常规配置文件使用。但是,在运行测试时必须提供解密配置文件的密钥。这可以通过标志/选项 decrypt_keydecrypt_file,或预定义位置的密钥文件来完成。

Common Test 还提供解密函数 ct:decrypt_config_file/2,3,用于重新创建原始文本文件。

使用配置数据打开连接

以下是使用例如 ct_sshct_ftpct_telnet 中的支持函数打开连接的两种不同方法:

  • 使用配置目标名称(别名)作为引用。
  • 使用配置变量作为引用。

当使用目标名称引用配置数据(指定要打开的连接)时,相同的名称可以用作与连接相关的所有后续调用(也用于关闭连接)中的连接标识。每个目标名称只能有一个打开的连接。如果您尝试使用已与打开的连接关联的名称打开新连接,Common Test 将返回已存在的句柄,以便使用先前打开的连接。此功能使得可以在任何有用的情况下调用打开特定连接的函数。除非需要,否则这样的操作不一定会打开任何新连接(例如,如果先前的连接被服务器意外关闭,则可能会出现这种情况)。使用命名连接还消除了在套件中传递这些连接的句柄引用的需要。

当使用配置变量名称作为引用来指定连接的数据时,打开连接返回的句柄必须在所有后续调用中使用(也用于关闭连接)。使用相同的变量名称作为引用重复调用打开函数会导致打开多个连接。这可能很有用,例如,如果测试用例需要在目标节点上打开与同一服务器的多个连接(每个连接使用相同的配置数据)。

用户特定的配置数据格式

用户可以指定与目前描述的文本文件中键值对不同的格式的配置数据。例如,可以从任何文件中读取数据、通过 HTTP 从 Web 获取数据,或者从用户特定的进程请求数据。为了支持这一点,Common Test 提供了一个回调模块插件机制来处理配置数据。

用于处理配置数据的默认回调模块

Common Test 包括用于处理以标准配置文件(如前所述)和 XML 文件指定的配置数据的默认回调模块,如下所示:

  • ct_config_plain - 用于读取带有键值对的配置文件(标准格式)。如果没有指定用户回调,则此处理程序用于解析配置文件。
  • ct_config_xml - 用于从 XML 文件读取配置数据。

使用 XML 配置文件

以下是 XML 配置文件的示例:

<config>
   <ftp_host>
       <ftp>"targethost"</ftp>
       <username>"tester"</username>
       <password>"letmein"</password>
   </ftp_host>
   <lm_directory>"/test/loadmodules"</lm_directory>
</config>

一旦读取,此文件将生成与以下文本文件相同的配置变量:

{ftp_host, [{ftp,"targethost"},
            {username,"tester"},
            {password,"letmein"}]}.

{lm_directory, "/test/loadmodules"}.

实现用户特定的处理程序

可以编写用户特定的处理程序来处理特殊的配置文件格式。该参数可以是文件名或配置字符串(空列表是有效的)。

实现处理程序的回调模块负责检查配置字符串的正确性。

要验证配置字符串,回调模块必须导出函数 Callback:check_parameter/1

输入参数从 Common Test 传递,如测试规范中定义,或指定为 ct_runct:run_test 的选项。

返回值应为以下任意值,指示指定的配置参数是否有效:

  • {ok, {file, FileName}} - 参数是一个文件名,并且该文件存在。
  • {ok, {config, ConfigString}} - 参数是一个配置字符串,并且它是正确的。
  • {error, {nofile, FileName}} - 当前目录中没有具有指定名称的文件。
  • {error, {wrong_config, ConfigString}} - 配置字符串错误。

应从回调模块导出函数 Callback:read_config/1,以便在测试开始之前或在测试执行期间重新加载数据的结果时读取配置数据。输入参数与函数 check_parameter/1 的输入参数相同。

返回值应为以下之一:

  • {ok, Config} - 如果成功读取配置变量。
  • {error, {Error, ErrorDetails}} - 如果回调模块无法继续执行指定的配置参数。

Config 是正确的 Erlang 键值列表,其中可能的键值子列表作为值,例如前面的配置文件示例:

[{ftp_host, [{ftp, "targethost"}, {username, "tester"}, {password, "letmein"}]},
 {lm_directory, "/test/loadmodules"}]

配置数据处理示例

以下是使用 FTP 客户端访问远程主机上的文件的配置文件示例:

{ftp_host, [{ftp,"targethost"},
            {username,"tester"},
            {password,"letmein"}]}.

{lm_directory, "/test/loadmodules"}.

也可以使用前面显示的 XML 版本,但必须明确指定 Common Test 使用 ct_config_xml 回调模块。

以下示例演示如何断言配置数据可用并可用于 FTP 会话:

init_per_testcase(ftptest, Config) ->
    {ok,_} = ct_ftp:open(ftp),
    Config.

end_per_testcase(ftptest, _Config) ->
    ct_ftp:close(ftp).

ftptest() ->
    [{require,ftp,ftp_host},
     {require,lm_directory}].

ftptest(Config) ->
    Remote = filename:join(ct:get_config(lm_directory), "loadmodX"),
    Local = filename:join(proplists:get_value(priv_dir,Config), "loadmodule"),
    ok = ct_ftp:recv(ftp, Remote, Local),
    ...

以下示例演示了如果需要打开与 FTP 服务器的多个连接,如何重写上一个示例中的函数:

init_per_testcase(ftptest, Config) ->
    {ok,Handle1} = ct_ftp:open(ftp_host),
    {ok,Handle2} = ct_ftp:open(ftp_host),
    [{ftp_handles,[Handle1,Handle2]} | Config].

end_per_testcase(ftptest, Config) ->
    lists:foreach(fun(Handle) -> ct_ftp:close(Handle) end,
                  proplists:get_value(ftp_handles,Config)).

ftptest() ->
    [{require,ftp_host},
     {require,lm_directory}].

ftptest(Config) ->
    Remote = filename:join(ct:get_config(lm_directory), "loadmodX"),
    Local = filename:join(proplists:get_value(priv_dir,Config), "loadmodule"),
    [Handle | MoreHandles] = proplists:get_value(ftp_handles,Config),
    ok = ct_ftp:recv(Handle, Remote, Local),
    ...

用户特定配置处理程序示例

一个简单的配置处理驱动程序,向外部服务器请求配置数据,可以按以下方式实现:

-module(config_driver).
-export([read_config/1, check_parameter/1]).

read_config(ServerName)->
    ServerModule = list_to_atom(ServerName),
    ServerModule:start(),
    ServerModule:get_config().

check_parameter(ServerName)->
    ServerModule = list_to_atom(ServerName),
    case code:is_loaded(ServerModule) of
        {file, _}->
            {ok, {config, ServerName}};
        false->
            case code:load_file(ServerModule) of
                {module, ServerModule}->
                    {ok, {config, ServerName}};
                {error, nofile}->
                    {error, {wrong_config, "File not found: " ++ ServerName ++ ".beam"}}
            end
    end.

如果以下 config_server.erl 模块已编译并在测试执行期间存在于代码路径中,则此驱动程序的配置字符串可以是 config_server

-module(config_server).
-export([start/0, stop/0, init/1, get_config/0, loop/0]).

-define(REGISTERED_NAME, ct_test_config_server).

start()->
    case whereis(?REGISTERED_NAME) of
        undefined->
            spawn(?MODULE, init, [?REGISTERED_NAME]),
            wait();
        _Pid->
        ok
    end,
    ?REGISTERED_NAME.

init(Name)->
    register(Name, self()),
    loop().

get_config()->
    call(self(), get_config).

stop()->
    call(self(), stop).

call(Client, Request)->
    case whereis(?REGISTERED_NAME) of
        undefined->
            {error, {not_started, Request}};
        Pid->
            Pid ! {Client, Request},
            receive
                Reply->
                    {ok, Reply}
            after 4000->
                {error, {timeout, Request}}
            end
    end.

loop()->
    receive
        {Pid, stop}->
            Pid ! ok;
        {Pid, get_config}->
            {D,T} = erlang:localtime(),
            Pid !
                [{localtime, [{date, D}, {time, T}]},
                 {node, erlang:node()},
                 {now, erlang:now()},
                 {config_server_pid, self()},
                 {config_server_vsn, ?vsn}],
            ?MODULE:loop()
    end.

wait()->
    case whereis(?REGISTERED_NAME) of
        undefined->
            wait();
        _Pid->
            ok
    end.

在这里,处理程序还提供了动态重新加载配置变量的功能。如果从测试用例函数调用 ct:reload_config(localtime),则使用 config_driver:read_config/1 加载的所有变量都会更新为其最新值,并返回变量 localtime 的新值。