Appium 的配置系统
Appium 2 支持 配置文件。配置文件旨在与命令行参数具有 (几乎) 1:1 的对应关系。最终用户可以为 Appium 2 提供配置文件、CLI 参数或两者 (参数优先于配置文件)。
本文档将对配置系统的工作原理进行技术概述。它面向 Appium 贡献者,但也将解释该系统的基本功能。
读取配置文件¶
配置文件是一个 JSON、JavaScript 或 YAML 文件,可以根据模式进行验证。默认情况下,此文件将命名为 .appiumrc.{json,js,yaml,yml}
,并且应该位于依赖于 appium
的项目的根目录中。其他文件名和位置通过 --config <file>
标志支持。出于显而易见的原因,config
参数在配置文件中是不允许的。
除了单独的文件之外,配置还可以嵌入到项目的 package.json
中,使用 appiumConfig
属性,例如:
当 Appium 服务器通过 appium
可执行文件启动时,lib/main.js
中的 init
函数将调用 lib/config-file.js
来加载和/或搜索配置文件以及 package.json
。
注意
如果找不到配置,则不会出错!
lilconfig
包提供了搜索和加载功能;有关搜索路径的更多信息,请参阅其文档。此外,Appium 通过 yaml
包提供对 YAML 编写的配置文件的支持。
如果找到配置文件并成功 验证,则结果将与一组默认值和任何其他 CLI 参数合并。CLI 参数优先于配置文件,配置文件优先于默认值。
验证¶
相同的系统用于同时验证配置文件和命令行参数。
ajv
包提供验证。当然,要让 ajv
验证任何内容,它必须提供一个模式。
基本模式是 JSON Schema Draft-7 兼容对象,由 lib/schema/appium-config-schema.js
导出。此模式定义了特定于 Appium 的配置,并且仅关注其作为服务器的行为;它没有定义任何其他功能的配置(例如,plugin
或 driver
子命令)。
警告
请注意,此文件是基本模式;这将变得非常重要。
此文件不是 JSON 文件,因为 a) JSON 对人类来说很难使用,b) 特别是 @jlipps 讨厌它,以及 c) ajv
接受对象,而不是 JSON 文件。
解释如何验证配置文件更简单,所以我们将从这里开始。
验证配置文件¶
当找到配置文件(lib/config-file.js
)时,它将使用配置文件的内容调用 lib/schema/schema.js
导出的 validate
函数。反过来,这会要求 ajv
根据 Appium 提供给它的模式验证数据。
如果配置文件无效,将生成错误以显示给用户。最后,init
函数将检测这些错误,显示它们,并且进程将退出。
我希望这说得通,因为这是简单的一部分。
验证 CLI 参数¶
如前所述,相同的系统用于验证配置文件和 CLI 参数。
完全不评判,但 Appium 使用 argparse
来解析其 CLI 参数。此包以及其他类似的包提供了一个 API 来定义命令行 Node.js 脚本接受的参数,并最终返回用户提供参数的对象表示形式。
就像模式定义了配置文件中允许的内容一样,它也定义了命令行上允许的内容。
通过模式定义 CLI 参数¶
必须定义 CLI 参数,然后才能验证其值。
JSON 模式不适合定义 CLI 参数——它需要一些润滑才能使其工作——但它足够接近,我们可以使用适配器和一些自定义元数据来做到这一点。
在 lib/cli/parser.js
中,有一个围绕 argparse
的 ArgumentParser
的包装器;它被称为(等等)... ArgParser
。包装器存在是因为我们对 argparse
做了一些自定义操作,但这与模式本身无关。
创建一个 ArgParser
实例并使用原始 CLI 参数调用其 parseArgs()
方法。接受参数的定义部分来自 lib/cli/args.js
——在这里,所有不打算与 server
子命令一起使用的参数都是硬编码的(例如,driver
子命令及其子命令)。args.js
还包含一个函数 getServerArgs()
,它反过来调用 lib/schema/cli-args.js
中的 toParserArgs
。lib/schema/cli-args.js
可以被认为是 argparse
和模式之间的“适配器”层。
toParserArgs
使用 lib/schema/schema.js
导出的 flattenSchema
函数,该函数将模式“压缩”成键/值表示形式。然后,toParserArgs
遍历每个键/值对,并将其“转换”成适合 ArgParser
的 ArgumentOption
对象。
此适配器(cli-args.js
)隐藏了大多数混乱;让我们更深入地探索这个老鼠窝。
CLI 和模式不一致¶
转换算法(参见 lib/schema/cli-args.js
中的函数 subSchemaToArgDef
)主要是巧妙地打包到一个函数中的黑客和特殊情况。从 argparse
到 JSON 模式无法干净映射的事项包括但不限于
- 模式无法在本地表达“将
--foo=<value>
的值存储在名为bar
的属性中”(这对应于ArgumentOption['dest']
属性)。 - 模式无法在本地表达别名;例如,
--verbose
也可以是-v
- 模式
enum
不限于多种类型,但argparse
的等效ArgumentOption['choices']
属性是 - 模式不知道
argparse
的“操作”概念(请注意,Appium 目前没有使用自定义操作——尽管它曾经使用过,并且它可以再次使用)。 argparse
没有email
、hostname
、ipv4
、uri
等的本机类型,而模式有- 模式验证只验证,它不执行翻译、转换或强制转换(大部分)。
argparse
允许这样做。 - 模式允许
null
类型,无论出于何种原因。您曾经在 CLI 上传递null
吗? argparse
不理解除基本类型以外的任何内容;没有对象、数组等,当然也没有特定类型的数组。
所有上述情况和其他情况都由适配器处理。
警告
适配器中的一些决定是通过抛硬币做出的。如果您好奇为什么某些事情是现在的样子,很可能是因为某些原因。
让我们更仔细地看看如何处理类型。
通过 ajv
的参数类型¶
虽然 argparse
允许消费者通过其 API 定义各种参数的类型(例如,字符串、数字、布尔标志等),但 Appium 大部分情况下避免了这些内置类型。为什么呢? 嗯
- 我们已经知道参数的类型,因为我们在模式中定义了它。
ajv
提供针对模式的验证。- 模式允许比
argparse
本身提供的更强大的类型、允许的值等表达。 - 模式的表达能力允许更好的错误消息。
为此,适配器放弃了argparse
的内置类型(参见ArgumentOption['type']
允许的字符串值),而是滥用提供函数作为type
的能力。例外是布尔标志,它们没有type
,而是action: 'store_true'
。世界可能永远不会知道为什么。
函数作为类型¶
当type
是一个函数时,该函数执行验证和强制转换(如果需要)。那么这些函数是什么呢?
注意:如果属性类型为
boolean
,则type
将被省略(因此不是函数),而是提供一个action
属性为store_true
。是的,这很奇怪。不,我不知道为什么。
好吧……这取决于模式。但一般来说,我们创建了一个管道函数,每个函数对应于模式中的一个关键字。让我们以port
参数为例。为了避免询问操作系统appium
运行的用户可以绑定到哪些端口,此参数预计是一个介于 1 和 65535 之间的整数。这变成了两个函数,我们将其组合成一个管道
- 如果可能,将值转换为整数。因为
process.argv
中的每个值都是一个字符串,所以如果我们想要一个数字,我们必须进行强制转换。 - 使用
ajv
根据port
的模式验证整数。模式允许我们通过minimum
和maximum
关键字定义一个范围。有关此工作原理的更多信息,请阅读
与配置文件验证类似,如果检测到错误,Appium 会很好地告知最终用户,并且进程会退出并提供一些帮助文本。
对于其他自然是非基本类型的参数,情况就不那么简单了。
转换器¶
还记得argparse
不理解数组吗?如果表达值的最佳方式实际上是一个数组呢?
好吧,Appium 无法在 CLI 上接受数组,即使它可以在配置文件中接受数组。但是 Appium 可以接受逗号分隔的字符串(CSV“行”)。或者一个字符串文件路径,该路径引用一个包含分隔列表的文件。无论哪种方式:当值从参数解析器中取出时,它应该是一个数组。
如上所述,JSON 模式本机设施无法表达这一点。但是,可以定义一个自定义关键字,Appium 可以检测并相应地处理。所以这就是 Appium 所做的。
在这种情况下,一个自定义关键字appiumCliTransformer
在ajv
中注册。appiumCliTransformer
的值(在撰写本文时)可以是csv
或json
。在基本模式文件appium-config-schema.js
中,Appium 使用appiumCliTransformer: 'csv'
(如果需要此行为)。
注意
在模式中定义的任何具有类型array
的属性将自动使用csv
转换器。同样,具有类型object
的属性将使用json
转换器。可以想象array
可能想要使用json
转换器,但除此之外,在array
或object
类型属性上存在appiumCliTransformer
关键字并不是严格必要的。
适配器(还记得适配器吗?)创建一个包含特殊“CSV 转换器”的管道函数(转换器在lib/schema/cli-transformers.js
中定义),并将此函数用作传递给argparse
的ArgumentOption
的type
属性。在这种情况下,模式中的type: 'array'
将被忽略。
注意
配置文件不需要执行任何复杂的值转换,因为它自然允许 Appium 定义它期望的准确内容。因此 Appium 不会对配置文件值进行任何后处理。
不需要这种特殊处理的属性直接使用ajv
进行验证。这如何工作需要一些解释,所以接下来就来解释一下。
通过ajv
验证单个参数¶
当我们想到 JSON 模式时,我们倾向于认为,“我有一个 JSON 文件,我想根据模式验证它”。这是有效的,事实上 Appium 对配置文件就是这样做的!但是,Appium 在验证参数时不会这样做。
注意
在实现过程中,我曾试图将所有参数合并成一个类似配置文件的数据结构,然后一次性验证它。我认为这是可能的,但由于一个充满 CLI 参数的对象是一个扁平的键/值结构,而模式不是,所以这似乎很麻烦。
相反,Appium 会根据模式中的特定属性验证一个值。为此,它维护着 CLI 参数定义与其对应属性之间的映射。映射本身是一个Map
,使用参数的唯一标识符作为键,使用ArgSpec
(lib/schema/arg-spec.js
)对象作为值。
ArgSpec
对象存储以下元数据
属性名称 | 描述 |
---|---|
name |
参数的规范名称,对应于模式中的属性名称。 |
extType? |
driver 或plugin ,如果适用 |
extName? |
扩展名,如果适用 |
ref |
计算出的模式中属性的$id |
arg |
CLI 上接受的参数,没有前导破折号 |
dest |
已解析参数对象中的属性名称(由argparse 的parse_args() 返回) |
defaultValue? |
模式中default 关键字的值,如果适用 |
当模式完成时,Map
将填充所有已知参数的ArgSpec
对象。
因此,当适配器为参数的type
创建管道函数时,它已经拥有该参数的ArgSpec
。它创建一个函数,该函数调用validate(value, ref)
(在lib/schema/schema.js
中),其中value
是用户提供的任何内容,而ref
是ArgSpec
的ref
属性。这个概念是ajv
可以使用它知道的任何ref
进行验证;模式中的每个属性都可以通过此ref
引用,无论它是否已定义。为了帮助可视化,如果模式是
foo
的ref
将是my-schema.json#/properties/foo
。假设我们的Ajv
实例知道这个my-schema.json
,那么我们可以调用它的getSchema(ref)
方法(它有一个schema
属性,但无论如何都是一个误称)来获取一个验证函数;schema.js
中的validate(value, ref)
调用此验证函数。
注意
模式规范指出,模式作者可以提供一个显式的$id
关键字来覆盖此内容;目前 Appium 不支持它。如果需要,扩展作者必须仔细使用$ref
,而无需自定义$id
。扩展的模式不太可能复杂到需要这样做,但是;Appium 本身甚至没有使用$ref
来定义自己的属性!
接下来,让我们看看 Appium 如何加载模式。这实际上发生在任何参数验证之前。
模式加载¶
让我们暂时忽略扩展,从基本模式开始。
当某件事第一次导入lib/schema/schema.js
模块时,将创建一个AppiumSchema
实例。这是一个单例,它的方法从模块导出(所有方法都绑定到该实例)。
构造函数几乎不做任何事情;它实例化一个Ajv
实例,并使用 Appium 的自定义关键字对其进行配置,并通过ajv-formats模块添加对format
关键字的支持。
否则,AppiumSchema
实例在调用其finalize()
方法(导出为finalizeSchema()
)之前不会与Ajv
实例交互。当调用此方法时,我们是在说“我们不会再添加任何模式了;继续创建ArgSpec
对象并将模式注册到ajv
”。
最终化何时发生?好吧
- 当
appium
可执行文件开始时,它检查并配置扩展(挥手)在APPIUM_HOME
中。 - 只有在那之后,它才会开始考虑参数——它实例化一个
ArgParser
,该解析器(如您所知)运行适配器将模式转换为参数。 - 最终化发生在这里——在创建解析器时。Appium 需要将模式注册到
ajv
,以便为参数创建验证函数。 - 此后,Appium 使用
ArgParser
解析参数。 - 最后,决定如何处理返回的对象。
如果没有扩展,finalize()
仍然知道 Appium 基本模式(appium-config-schema.js
),并且只注册它。但是,步骤 1. 正在执行大量工作,所以让我们看看扩展是如何发挥作用的。
扩展支持¶
此系统的设计目标之一如下
扩展应该能够向 Appium 注册自定义 CLI 参数,用户应该能够像使用任何其他参数一样使用它们.
以前,Appium 2 以这种方式接受参数(通过--driverArgs
),但验证是手工完成的,并且要求扩展实现者使用自定义 API。它还要求用户在命令行上尴尬地传递一个 JSON 字符串作为配置。此外,这些参数不存在上下文帮助(通过--help
)。
现在,通过为其选项提供模式,驱动程序或插件可以向 Appium 注册 CLI 参数和配置文件模式。
要注册模式,扩展必须在其package.json
中提供appium.schema
属性。该值可以是模式或模式的路径。如果是后者,模式应该是 JSON 或 CommonJS 模块(目前不支持 ESM,也不支持 YAML)。
对于此模式中的任何属性,该属性将显示为以下形式的 CLI 参数:--<extension-type>-<extension-name>-<property-name>
。例如,如果fake
驱动程序提供一个属性foo
,则该参数将是--driver-fake-foo
,并且将在appium server --help
中显示,就像任何其他 CLI 参数一样。
配置文件中的对应属性将是server.<extension-type>.<extension-name>.<property-name>
,例如
上面描述的命名约定避免了以下问题:一种扩展类型与另一种扩展类型具有名称冲突。
注意
虽然扩展可以通过appiumCliAliases
提供别名,但“短”标志是不允许的,因为来自扩展的所有参数都以--<extension-type>-<extension-name>-
为前缀。扩展名和参数名将根据Lodash 关于 kebab-casing 的规则在 CLI 上进行 kebab-casing。
模式对象看起来很像 Appium 的基本模式,但它只具有顶级属性(目前不支持嵌套属性)。例如
{
"title": "my rad schema for the cowabunga driver",
"type": "object",
"properties": {
"fizz": {
"type": "string",
"default": "buzz",
"$comment": "corresponds to CLI --driver-cowabunga-fizz"
}
}
}
如用户配置文件中所写,这将是server.driver.cowabunga.fizz
属性。
当加载扩展时,将验证schema
属性,并将该模式注册到AppiumSchema
(在调用finalize()
之前,它不会注册到Ajv
)。
在最终化期间,每个注册的模式都将添加到Ajv
实例中。该模式将根据扩展类型和名称分配一个$id
(这将覆盖扩展提供的任何内容,如果有的话)。模式还被强制通过additionalProperties: false
关键字来禁止未知参数。
在幕后,基本模式具有driver
和plugin
属性,它们是对象。最终化时,将向每个属性添加一个属性——对应于扩展名——该属性的值是对扩展模式中属性的$id
的引用。例如,server.driver
属性将如下所示
这就是我们称之为“基本”模式的原因——当扩展提供模式时,它会被修改。扩展模式将单独保留,但引用将在模式最终添加到ajv
之前添加到模式中。这之所以有效,是因为Ajv
实例理解它知道的任何模式到它知道的任何模式的引用。
注意
这使得为 Appium *和* 已安装的扩展提供完整的静态模式变得不可能(截至 2021 年 11 月 5 日)。一个静态的 .json
模式 *是* 从基础(通过 Gulp 任务)生成的,但它不包含任何扩展模式。静态模式在 Appium 之外也有用途;例如,IDE 可以通过这种方式提供配置文件的上下文错误检查。我们来解决这个问题吧?
就像我们在基本模式中查找特定参数的引用 ID 一样,对来自扩展的参数的验证也是以完全相同的方式进行的。如果 cowabunga
驱动程序的模式 ID 为 driver-cowabunga.json
,那么 fizz
属性可以通过 driver-cowabunga.json#/properties/fizz
从使用 ajv
注册的任何模式中引用。“基本”模式参数以 appium.json#properties/
开头。
开发环境支持¶
在开发流程中,已经自动化了一些额外的任务来维护基本模式
- 作为转译后的步骤,一个
lib/appium-config.schema.json
从 lib/schema/appium-config-schema.js
(除了由 Babel 生成的 CJS 对应文件)生成。- 此文件在版本控制之下。它最终被 *复制* 到
build/lib/appium-config.schema.json
中。一个预提交钩子(参见- 根 monorepo 中的
scripts/generate-schema-declarations.js
)从上面的 JSON 文件生成 - 一个
types/appium-config-schema.d.ts
。types/types.d.ts
中的类型 - 依赖于此文件。此文件在版本控制之下。
自定义关键字参考¶
关键字在 lib/schema/keywords.js
中定义。
appiumCliAliases
:允许模式表达别名(例如,CLI 参数可以是--verbose
或-v
)。这是一个字符串数组。小于三个 (3) 个字符的字符串将以单个连字符 (-
) 而不是双连字符 (--
) 开头。请注意,扩展提供的任何参数都将以双连字符开头,因为这些参数需要具有--<extension-type>-<extension-name>-
前缀。appiumCliDest
:允许模式在argprase
后处理的参数对象中指定自定义属性名称。如果未设置,这将成为一个驼峰式字符串。appiumCliDescription
:允许模式在命令行上显示时覆盖参数的描述。这与appiumCliTransformer
(或array
/object
类型属性)配对非常有用,因为 CLI 用户可以提供的内容与配置文件用户可以提供的内容之间存在很大差异。appiumCliTransformer
:目前可以选择csv
和json
。这些是自定义函数,它们对值进行后处理。它们在加载和验证配置文件时不会使用,但想法应该是它们会生成与使用配置文件所需的对象相同的对象(例如,字符串数组)。csv
用于逗号分隔的字符串和 CSV 文件;json
用于原始 JSON 字符串和.json
文件。appiumCliIgnore
:如果为true
,则不支持 CLI 上的此属性。appiumDeprecated
:如果为true
,则该属性被认为是“已弃用”,并将向用户显示为已弃用(例如,在--help
输出中)。请注意,JSON 模式草案 2019-09 引入了一个新的关键字deprecated
,如果升级到此元模式,我们应该使用它。这样做时,appiumDeprecated
本身应该被标记为deprecated
。