跳至内容

Appium 的配置系统

Appium 2 支持 配置文件。配置文件旨在与命令行参数具有 (几乎) 1:1 的对应关系。最终用户可以为 Appium 2 提供配置文件、CLI 参数或两者 (参数优先于配置文件)。

本文档将对配置系统的工作原理进行技术概述。它面向 Appium 贡献者,但也将解释该系统的基本功能。

读取配置文件

配置文件是一个 JSON、JavaScript 或 YAML 文件,可以根据模式进行验证。默认情况下,此文件将命名为 .appiumrc.{json,js,yaml,yml},并且应该位于依赖于 appium 的项目的根目录中。其他文件名和位置通过 --config <file> 标志支持。出于显而易见的原因,config 参数在配置文件中是不允许的。

除了单独的文件之外,配置还可以嵌入到项目的 package.json 中,使用 appiumConfig 属性,例如:

{
  "appiumConfig": {
    "server": {
      "port": 12345
    }
  }
}

当 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 的配置,并且仅关注其作为服务器的行为;它没有定义任何其他功能的配置(例如,plugindriver 子命令)。

警告

请注意,此文件是基本模式;这将变得非常重要。

此文件不是 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 中,有一个围绕 argparseArgumentParser 的包装器;它被称为(等等)... ArgParser。包装器存在是因为我们对 argparse 做了一些自定义操作,但这与模式本身无关。

创建一个 ArgParser 实例并使用原始 CLI 参数调用其 parseArgs() 方法。接受参数的定义部分来自 lib/cli/args.js——在这里,所有打算与 server 子命令一起使用的参数都是硬编码的(例如,driver 子命令及其命令)。args.js 还包含一个函数 getServerArgs(),它反过来调用 lib/schema/cli-args.js 中的 toParserArgslib/schema/cli-args.js 可以被认为是 argparse 和模式之间的“适配器”层。

toParserArgs 使用 lib/schema/schema.js 导出的 flattenSchema 函数,该函数将模式“压缩”成键/值表示形式。然后,toParserArgs 遍历每个键/值对,并将其“转换”成适合 ArgParserArgumentOption 对象。

此适配器(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 没有 emailhostnameipv4uri 等的本机类型,而模式有
  • 模式验证只验证,它不执行翻译、转换或强制转换(大部分)。argparse 允许这样做。
  • 模式允许 null 类型,无论出于何种原因。您曾经在 CLI 上传递 null 吗?
  • argparse 不理解除基本类型以外的任何内容;没有对象、数组等,当然也没有特定类型的数组。

所有上述情况和其他情况都由适配器处理。

警告

适配器中的一些决定是通过抛硬币做出的。如果您好奇为什么某些事情是现在的样子,很可能是因为某些原因。

让我们更仔细地看看如何处理类型。

通过 ajv 的参数类型

虽然 argparse 允许消费者通过其 API 定义各种参数的类型(例如,字符串、数字、布尔标志等),但 Appium 大部分情况下避免了这些内置类型。为什么呢?

  1. 我们已经知道参数的类型,因为我们在模式中定义了它。
  2. ajv 提供针对模式的验证。
  3. 模式允许比 argparse 本身提供的更强大的类型、允许的值等表达。
  4. 模式的表达能力允许更好的错误消息。

为此,适配器放弃了argparse的内置类型(参见ArgumentOption['type']允许的字符串值),而是滥用提供函数作为type的能力。例外是布尔标志,它们没有type,而是action: 'store_true'。世界可能永远不会知道为什么。

函数作为类型

type是一个函数时,该函数执行验证强制转换(如果需要)。那么这些函数是什么呢?

注意:如果属性类型为boolean,则type将被省略(因此不是函数),而是提供一个action属性为store_true。是的,这很奇怪。不,我不知道为什么。

好吧……这取决于模式。但一般来说,我们创建了一个管道函数,每个函数对应于模式中的一个关键字。让我们以port参数为例。为了避免询问操作系统appium运行的用户可以绑定到哪些端口,此参数预计是一个介于 1 和 65535 之间的整数。这变成了两个函数,我们将其组合成一个管道

  1. 如果可能,将值转换为整数。因为process.argv中的每个值都是一个字符串,所以如果我们想要一个数字,我们必须进行强制转换。
  2. 使用ajv根据port的模式验证整数。模式允许我们通过minimummaximum关键字定义一个范围。有关此工作原理的更多信息,请阅读

与配置文件验证类似,如果检测到错误,Appium 会很好地告知最终用户,并且进程会退出并提供一些帮助文本。

对于其他自然是非基本类型的参数,情况就不那么简单了。

转换器

还记得argparse不理解数组吗?如果表达值的最佳方式实际上是一个数组呢?

好吧,Appium 无法在 CLI 上接受数组,即使它可以在配置文件中接受数组。但是 Appium 可以接受逗号分隔的字符串(CSV“行”)。或者一个字符串文件路径,该路径引用一个包含分隔列表的文件。无论哪种方式:当值从参数解析器中取出时,它应该是一个数组。

如上所述,JSON 模式本机设施无法表达这一点。但是,可以定义一个自定义关键字,Appium 可以检测并相应地处理。所以这就是 Appium 所做的。

在这种情况下,一个自定义关键字appiumCliTransformerajv中注册。appiumCliTransformer的值(在撰写本文时)可以是csvjson。在基本模式文件appium-config-schema.js中,Appium 使用appiumCliTransformer: 'csv'(如果需要此行为)。

注意

在模式中定义的任何具有类型array的属性将自动使用csv转换器。同样,具有类型object的属性将使用json转换器。可以想象array可能想要使用json转换器,但除此之外,在arrayobject类型属性上存在appiumCliTransformer关键字并不是严格必要的。

适配器(还记得适配器吗?)创建一个包含特殊“CSV 转换器”的管道函数(转换器在lib/schema/cli-transformers.js中定义),并将此函数用作传递给argparseArgumentOptiontype属性。在这种情况下,模式中的type: 'array'将被忽略。

注意

配置文件不需要执行任何复杂的值转换,因为它自然允许 Appium 定义它期望的准确内容。因此 Appium 不会对配置文件值进行任何后处理。

不需要这种特殊处理的属性直接使用ajv进行验证。这如何工作需要一些解释,所以接下来就来解释一下。

通过ajv验证单个参数

当我们想到 JSON 模式时,我们倾向于认为,“我有一个 JSON 文件,我想根据模式验证它”。这是有效的,事实上 Appium 对配置文件就是这样做的!但是,Appium 在验证参数时不会这样做。

注意

在实现过程中,我曾试图将所有参数合并成一个类似配置文件的数据结构,然后一次性验证它。我认为这是可能的,但由于一个充满 CLI 参数的对象是一个扁平的键/值结构,而模式不是,所以这似乎很麻烦。

相反,Appium 会根据模式中的特定属性验证一个值。为此,它维护着 CLI 参数定义与其对应属性之间的映射。映射本身是一个Map,使用参数的唯一标识符作为键,使用ArgSpeclib/schema/arg-spec.js)对象作为值。

ArgSpec对象存储以下元数据

属性名称 描述
name 参数的规范名称,对应于模式中的属性名称。
extType? driverplugin,如果适用
extName? 扩展名,如果适用
ref 计算出的模式中属性的$id
arg CLI 上接受的参数,没有前导破折号
dest 已解析参数对象中的属性名称(由argparseparse_args()返回)
defaultValue? 模式中default关键字的值,如果适用

当模式完成时,Map将填充所有已知参数的ArgSpec对象。

因此,当适配器为参数的type创建管道函数时,它已经拥有该参数的ArgSpec。它创建一个函数,该函数调用validate(value, ref)(在lib/schema/schema.js中),其中value是用户提供的任何内容,而refArgSpecref属性。这个概念是ajv可以使用它知道的任何ref进行验证;模式中的每个属性都可以通过此ref引用,无论它是否已定义。为了帮助可视化,如果模式是

{
  "$id": "my-schema.json",
  "type": "object",
  "properties": {
    "foo": {
      "type": "number"
    }
  }
}

fooref将是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”。

最终化何时发生?好吧

  1. appium可执行文件开始时,它检查并配置扩展(挥手)在APPIUM_HOME中。
  2. 只有在那之后,它才会开始考虑参数——它实例化一个ArgParser,该解析器(如您所知)运行适配器将模式转换为参数。
  3. 最终化发生在这里——在创建解析器时。Appium 需要将模式注册到ajv,以便为参数创建验证函数。
  4. 此后,Appium 使用ArgParser解析参数。
  5. 最后,决定如何处理返回的对象。

如果没有扩展,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>,例如

{
  "server": {
    "driver": {
      "fake": {
        "foo": "bar"
      }
    }
  }
}

上面描述的命名约定避免了以下问题:一种扩展类型与另一种扩展类型具有名称冲突。

注意

虽然扩展可以通过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关键字来禁止未知参数。

在幕后,基本模式具有driverplugin属性,它们是对象。最终化时,将向每个属性添加一个属性——对应于扩展名——该属性的值是对扩展模式中属性的$id的引用。例如,server.driver属性将如下所示

{
  "driver": {
    "cowabunga": {
      "$ref": "driver-cowabunga.json"
    }
  }
}

这就是我们称之为“基本”模式的原因——当扩展提供模式时,它会被修改。扩展模式将单独保留,但引用将在模式最终添加到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.tstypes/types.d.ts 中的类型
  • 依赖于此文件。此文件在版本控制之下。

自定义关键字参考

关键字在 lib/schema/keywords.js 中定义。

  • appiumCliAliases:允许模式表达别名(例如,CLI 参数可以是 --verbose-v)。这是一个字符串数组。小于三个 (3) 个字符的字符串将以单个连字符 (-) 而不是双连字符 (--) 开头。请注意,扩展提供的任何参数都将以双连字符开头,因为这些参数需要具有 --<extension-type>-<extension-name>- 前缀。
  • appiumCliDest:允许模式在 argprase 后处理的参数对象中指定自定义属性名称。如果未设置,这将成为一个驼峰式字符串。
  • appiumCliDescription:允许模式在命令行上显示时覆盖参数的描述。这与 appiumCliTransformer(或 array/object 类型属性)配对非常有用,因为 CLI 用户可以提供的内容与配置文件用户可以提供的内容之间存在很大差异。
  • appiumCliTransformer:目前可以选择 csvjson。这些是自定义函数,它们对值进行后处理。它们在加载和验证配置文件时不会使用,但想法应该是它们会生成与使用配置文件所需的对象相同的对象(例如,字符串数组)。csv 用于逗号分隔的字符串和 CSV 文件;json 用于原始 JSON 字符串和 .json 文件。
  • appiumCliIgnore:如果为 true,则不支持 CLI 上的此属性。
  • appiumDeprecated:如果为 true,则该属性被认为是“已弃用”,并将向用户显示为已弃用(例如,在 --help 输出中)。请注意,JSON 模式草案 2019-09 引入了一个新的关键字 deprecated,如果升级到此元模式,我们应该使用它。这样做时,appiumDeprecated 本身应该被标记为 deprecated