跳至內容

Appium 的設定系統

Appium 2 支援 設定檔。設定檔的用意是與命令列參數有 (幾乎) 1:1 的對應關係。最終使用者可以提供設定檔、CLI 參數或同時提供兩者給 Appium 2(參數優先於設定檔)。

本文件將技術性地概述設定系統的運作方式。本文件是寫給 Appium 貢獻者的,但也會說明系統的基本功能。

讀取設定檔

設定檔是 JSON、JavaScript 或 YAML 檔,可以針對架構進行驗證。預設情況下,此檔會命名為 .appiumrc.{json,js,yaml,yml},且應位於依賴於 appium 的專案根目錄中。其他檔名和位置可透過 --config <file> 旗標支援。基於顯而易見的原因,設定檔中不允許使用 config 參數。

除了獨立的檔案之外,也可以使用 appiumConfig 屬性將設定嵌入專案的 package.json 中,例如:

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

當透過 appium 可執行檔啟動 Appium 伺服器時,lib/main.js 中的 init 函式會呼叫 lib/config-file.js 來載入和/或搜尋設定檔和 package.json

注意

如果找不到設定,並非錯誤!

lilconfig 套件提供搜尋和載入功能;有關搜尋路徑的詳細資訊,請參閱其文件。此外,Appium 透過套件 yaml 提供支援,以 YAML 編寫的設定檔。

如果找到設定檔並成功驗證,結果將與一組預設值和任何其他 CLI 參數合併。CLI 參數優先於設定檔,而設定檔優先於預設值。

驗證

相同的系統用於設定檔命令列參數的驗證。

套件 ajv 提供驗證。當然,要讓 ajv 驗證任何內容,必須提供架構

基本架構是 lib/schema/appium-config-schema.js 匯出的 JSON 架構草案 7 相容物件。此架構定義 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 腳本接受的參數,並最終會傳回使用者提供的參數的物件表示。

就像架構定義設定檔中允許的內容一樣,它也定義命令列中允許的內容。

透過 Schema 定義 CLI 參數

在驗證 CLI 參數值之前,必須定義這些參數。

JSON Schema 並非定義 CLI 參數的理想選擇,需要一些技巧才能讓它發揮作用,但它已經接近我們的需求,只要使用轉接器和一些自訂的元資料即可。

lib/cli/parser.js 中,有一個包覆 argparseArgumentParser 的包裝器;它稱為 (等一下)... ArgParser。這個包裝器存在是因為我們對 argparse 進行了一些自訂操作,但它與 Schema 本身無關。

會建立一個 ArgParser 實例,並使用原始 CLI 參數呼叫其 parseArgs() 方法。已接受參數的定義部分來自 lib/cli/args.js,其中所有打算與 server 子指令一起使用的參數都是寫死的 (例如,driver 子指令及其子指令)。args.js 還包含一個函式 getServerArgs(),它會呼叫 lib/schema/cli-args.js 中的 toParserArgslib/schema/cli-args.js 可視為 argparse 和 Schema 之間的「轉接器」層。

toParserArgs 使用 lib/schema/schema.js 匯出的 flattenSchema 函式,將 Schema「壓縮」成鍵/值表示法。然後,toParserArgs 會逐一迭代每個鍵/值對,並將其「轉換」成適合的 ArgumentOption 物件,最後交給 ArgParser

這個轉接器 (cli-args.js) 隱藏了大部分的混亂;讓我們進一步探索這個老鼠窩。

CLI 和 Schema 的不一致性

轉換演算法 (請參閱 lib/schema/cli-args.js 中的 subSchemaToArgDef 函式) 主要只是將技巧和特殊情況整齊地打包成一個函式。無法從 argparse 清楚對應到 JSON Schema 的內容包括,但不限於

  • Schema 無法原生表達「將 --foo=<value> 的值儲存在名為 bar 的屬性中」(這對應到 ArgumentOption['dest'] 屬性)。
  • Schema 無法原生表達別名;例如,--verbose 也可能是 -v
  • Schema enum 不限於多種類型,但 argparse 等效的 ArgumentOption['choices'] 屬性限制
  • Schema 不了解 argparse 的「動作」概念 (請注意,Appium 目前未使用自訂動作,但它曾經使用過,而且未來可能還會再使用)。
  • argparse 沒有原生類型可用於 emailhostnameipv4uri 等,但 Schema 有
  • 架構驗證只會驗證,它不會執行轉換、變換或強制轉換(大部分情況下)。argparse 允許這樣做。
  • 架構允許 null 類型,無論出於何種原因。是否曾透過 CLI 傳遞 null
  • argparse 僅了解基本類型,不了解物件、陣列等,當然也不了解特定類型的陣列。

上述所有情況和其他情況都由轉接器處理。

警告

轉接器中做出的一些決策是透過擲硬幣做出的。如果您好奇為什麼某些事物會以某種方式呈現,則很可能是因為它必須做些什麼

讓我們更仔細地了解處理類型。

透過 ajv 處理引數類型

雖然 argparse 允許使用者透過其 API 定義各種引數的類型(例如字串、數字、布林旗標等),但 Appium 大多避免使用這些內建類型。為什麼會這樣?

  1. 我們已經知道引數的類型,因為我們已在架構中定義了它。
  2. ajv 提供針對架構的驗證。
  3. argparse 本身提供的功能相比,架構允許更廣泛地表達類型、允許的值等。
  4. 架構的表達能力允許更好的錯誤訊息。

為此,轉接器迴避了 argparse 的內建類型(請參閱 ArgumentOption['type'] 的允許字串值),而是濫用提供函式作為類型的能力。例外情況是布林旗標,它沒有類型,而是有 action: 'store_true'。這個世界可能永遠不知道為什麼。

類型作為函式

類型是函式時,函式會執行驗證強制轉換(如果需要)。那麼這些函式是什麼?

注意:如果屬性類型為 boolean,則會從 ArgumentOption省略(因此不是函式),而是提供 store_trueaction 屬性。是的,這很奇怪。不,我不知道為什麼。

嗯...這取決於架構。但一般來說,我們會建立一個函式管線,每個函式都對應於架構中的關鍵字。我們以 port 引數為例。這個引數預期為 1 到 65535 之間的整數,而不是詢問作業系統 appium 執行使用者可以繫結到哪些埠。這變成兩個函式,我們將它們組合成一個管線

  1. 如果可能,將值轉換為整數。由於process.argv 中的每個值都是字串,因此如果我們想要一個數字,我們必須強制轉換。
  2. 使用 ajv 根據 port 的架構驗證整數。架構允許我們透過 minimummaximum 關鍵字定義範圍。在中閱讀更多關於如何執行此操作的資訊

與設定檔驗證很像,如果偵測到錯誤,Appium 會親切地告訴最終使用者,且程序會退出並提供一些說明文字。

對於其他非原始類型的引數,事情就沒那麼簡單了。

轉換器

還記得 argparse 不懂陣列嗎?如果表達值的最佳人體工學方式實際上是陣列,那該怎麼辦?

嗯,Appium 無法在 CLI 上接受陣列,即使它可以在設定檔中接受陣列。但 Appium 可以 接受逗號分隔的字串(CSV「列」)。或是一個字串檔案路徑,指向包含分隔清單的檔案。無論哪種方式:當值離開引數剖析器時,它都應該是陣列。

而且如上所述,JSON 架構的原生功能無法表達這一點。但是,可以定義一個自訂關鍵字,然後 Appium 可以偵測並適當地處理。所以這就是 Appium 所做的。

在這種情況下,自訂關鍵字 appiumCliTransformer 已向 ajv 註冊。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,其中參數的唯一識別碼為鍵,而 ArgSpec (lib/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 執行個體不會與 Ajv 執行個體互動,直到呼叫其 finalize() 方法(匯出為 finalizeSchema())。當呼叫此方法時,我們表示「我們不會再新增任何架構;繼續建立 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)。

對於此架構中的任何屬性,該屬性將顯示為 --<extension-type>-<extension-name>-<property-name> 格式的 CLI 參數。例如,如果 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 的規則 以連字號形式套用於 CLI 中的連字號大小寫。

架構物件看起來很像 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,則可以透過 driver-cowabunga.json#/properties/fizz 從向 ajv 註冊的任何架構參考 fizz 屬性。「基礎」架構參數以 appium.json#properties/ 開頭。

開發環境支援

在開發流程中,已自動執行一些額外任務來維護基礎架構

  • 作為轉譯後步驟,lib/appium-config.schema.json 會從
  • lib/schema/appium-config-schema.js(除了 Babel 產生的 CJS 對應檔之外)產生。
  • 這個檔案受版本控制。它最後會複製
  • build/lib/appium-config.schema.json 中。一個提交前掛鉤(請參閱
  • 單一儲存庫根目錄中的 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 Schema draft-2019-09 引入了新的關鍵字 deprecated,如果升級到此元架構,我們應該改用此關鍵字。這樣做時,appiumDeprecated 本身應該標記為 deprecated