Conformance test #1 について (実行コマンド生成編)

# 全体の構成
- [前振り編](https://qiita.com/tm_tn/private/d49195d1c06143fee5c2)
    - #1 でテストされる入力ファイルおよび期待される出力の説明
- 実行コマンド生成編 (いまここ)
    - #1 で実行されるコマンドを CWL ファイルから生成する方法の解説
- [Docker & 出力パラメータ編](https://qiita.com/tm_tn/items/a32064f01769bb05f45d)
    - 実行コマンド生成編では取り扱わなかった、Docker 上でコマンドを動かす場合の解説および、出力パラメータの補足方法の解説

また、[ダイジェスト版の説明スライド](https://speakerdeck.com/ttanjo/how-to-pass-the-conformance-test-number-1)(英語)もあります。

# 注意！

この記事は CWL に対応したワークフローエンジンを実装する人向けの記事です！
CWL を書いて利用する一般の方は「中の人は大変そうだなぁ」と思いつつ、お茶でも飲みながら軽い気持ちで読んでいただければ幸いです。
実装する人は心を強く持ちましょう。

# つづき

昨日の記事では「僕の考えた最強のワークフローエンジン」の最初の(そしてかなり大きな)壁である conformance test #1 の入力パラメータや出力例などを簡単に示しました。

> 次回は、このテストの突破に必要な仕様の解説を行います。

と書きましたが、分量が増えたので、今回は `DockerRequirement` と出力パラメータを適用する前の段階まで解説を行います。

# 早速解説
[4.1 節](https://www.commonwl.org/v1.0/CommandLineTool.html#Input_binding)に CWL での実行コマンド生成について記述があります。
以下が概要です。各番号は 4.1 節での手順に対応しています。

1. `arguments` から `CommandLineBinding` オブジェクトを収集する。
2. `inputs` から `CommandLineBinding` オブジェクトを収集し、入力オブジェクトをそれぞれ関連付ける。
3. `inputs` から収集した `CommandLineBinding` にソート用のキーを対応付ける
4. いい感じにソートする。 
5. 各 `CommandLineBinding` オブジェクトに対して、引数生成ルールを適用する。
6. 先頭に `baseCommand` を挿入して完成！

## 1. `arguments` から `CommandLineBinding` オブジェクトを収集する (いきなり仕様にない: [#806](https://github.com/common-workflow-language/common-workflow-language/issues/806))
[4.1 節](https://www.commonwl.org/v1.0/CommandLineTool.html#Input_binding) の 1 は以下になります。
> Collect `CommandLineBinding` objects from `arguments`. Assign a sorting key `[position, i]` where `position` is `CommandLineBinding.position` and `i` is the index in the `arguments` list.

`CommandLineBinding` は、[5.1.2 節](https://www.commonwl.org/v1.0/CommandLineTool.html#CommandLineBinding)　で定義されたフィールドを持つオブジェクトのことです。今回の例では、最後の要素がそれに当たります。
じゃあ最初の2つはなんなんだ…？と思って仕様を確認すると、`arguments` の要素として文字列も来る場合があると[記述があります](https://www.commonwl.org/v1.0/CommandLineTool.html#CommandLineTool)が、`arguments` に `CommandLineBinding` 以外が来る場合の処理については**どこにも記述がありません**。

```yaml:v1.0/bwa-mem-tool.cwl
arguments:
  - bwa
  - mem
  - valueFrom: $(runtime.cores)
    position: 1
    prefix: -t
```

CWL project leader の  Michael R. Crusoe 氏に確認すると、以下のような `CommandLineBinding` と置き換えても実装上は問題ないとのことでした。

```yaml:同値なarguments
arguments:
  - valueFrom: bwa
  - valueFrom: mem
  - valueFrom: $(runtime.cores)
    position: 1
    prefix: -t
```

これについては [Issue #806](https://github.com/common-workflow-language/common-workflow-language/issues/806) としてすでに登録されているので、v1.1 では明文化されるはずです。以下では `"bwa"` と `"mem"` を、上記の同値な `CommandLineBinding`オブジェクトとして取り扱います。

次に、各要素とソート用のキーを割り当てます。
[5.1.2 節](https://www.commonwl.org/v1.0/CommandLineTool.html#CommandLineBinding)の `position` の項を見ると、

> The sorting key. Default position is 0.

とあることから、以下のようにキーと `CommandLineBinding` オブジェクトを関連付けられます。

|キー(`[position, i]`)|`CommandLineBinding` オブジェクト|
|---|---|
|`[0, 0]`| `"bwa"`|
|`[0, 1]`| `"mem"`|
|`[1, 2]`| `{ "valueFrom": ...}`|


## 2. `inputs` から `CommandLineBinding` オブジェクトを収集し、入力オブジェクトをそれぞれ関連付ける。

以下が [4.1 節](https://www.commonwl.org/v1.0/CommandLineTool.html#Input_binding) の 2 です。

> Collect `CommandLineBinding` objects from the `inputs` schema and associate them with values from the input object. Where the input type is a record, array, or map, recursively walk the schema and input object, collecting nested `CommandLineBinding` objects and associating them with values from the input object.

`inputBinding` 以下が、`inputs` の `CommandLineBinding` オブジェクトです。
また `bwa-mem-job.json` が入力オブジェクトです。

まずは入力オブジェクトを見ていきましょう。

### `default` パラメータ
入力オブジェクトを見ると、フィールドが4つあることがわかります。

```json:v1.0/bwa-mem-job.json
{
    "reference": ...,
    "reads": ...,
    "min_std_max_min": ...,
    "minimum_seed_length": ...
}
```

それぞれ CWL ファイルの `inputs` の各フィールドが対応しているはずなのですが…

```yaml:v1.0/bwa-mem-tool.cwl
inputs:
  - id: reference
    ...
  - id: reads
    ...
  - id: minimum_seed_length
    ...
  - id: min_std_max_min
    ...
  - id: args.py # <------------ !?
    ...
```
あぁん…？

#### 何が起きてるの
入力パラメータ `args.py` を詳しく見ると、以下のようになっています。

```yaml:v1.0/bwa-mem-tool.cwl
inputs:
  ...
  - id: args.py
    type: File
    default:
      class: File
      location: args.py
  ...
```

[CommandLineTool の 5.0 節](https://www.commonwl.org/v1.0/CommandLineTool.html#CommandLineTool)には以下の記述があります。
> If an input parameter is missing from the input object, it must be assigned a value of `null` (or the value of `default` for that parameter, if provided) ...

そのため `default` フィールドで指定された `args.py` ファイル (厳密には `v1.0/bwa-mem-tool.cwl` と同一ディレクトリにある `args.py`) が代わりに `args.py` パラメータの値として使用されます。パラメータ名とファイル名が同じなのでややこしい…

### 無視される入力パラメータのフィールド (仕様にない！: [#808](https://github.com/common-workflow-language/common-workflow-language/issues/808), [#811](https://github.com/common-workflow-language/common-workflow-language/pull/811))

さて、今度は `reference` パラメータを見てみましょう。

```json:v1.0/bwa-mem-job.json
{
    "reference": {
        "class": "File",
        "location": "chr20.fa",
        "size": 123,
        "checksum": "sha1$hash"
    },
    ...
}
```

「`"size": 123`とあるから中身があるファイルなのね」と思い、実際に中身を確認すると…

```console
$ wc -c v1.0/chr20.fa
0
```

中身は空です。それによく見たら `checksum` も有効な sha1 ハッシュ値ではありません。
これをどう扱うべきか仕様を確認すると…

**どこにも記述がない**

- 本来的には: 仕様にどうなるべきか(あるいは動作が実装依存だと)記述があるべき
- 現状: 仕様に一切記述がないが、このテストはパスする意図で書かれている。おそらく入力パラメータの `File` オブジェクトの `size` や `checksum` 等のパラメータは無視していいのだろう…
    - CWL project leader の  Michael R. Crusoe 氏に確認したら、clarify すべきという Issue と Pull Request が立ちました([#808](https://github.com/common-workflow-language/common-workflow-language/issues/808), [#811](https://github.com/common-workflow-language/common-workflow-language/pull/811))。
    - v1.1 では明文化されるはずです。仕様が公開されていると、Issue などで現状どうなっているか確認できるので便利ですね！

今回は、入力オブジェクトの `size` や `checksum` が間違っていても受け付けてくれるゆるい処理系を前提にして話を進めます[^1]。

[^1]: この場合の挙動について仕様には記載がないため、この不届きな入力に対して `permanentFailure` で落ちる場合も、それとなく無視して実行される場合も仕様に従った動作と言える可能性があります。皆さんは(動作が処理系依存になりうる)このような入力ファイルは用意しないようにしましょうね！


## 3. `inputs` から収集した `CommandLineBinding` にソート用のキーを対応付ける

以下が[4.1 節](https://www.commonwl.org/v1.0/CommandLineTool.html#Input_binding) の 3 項です。
> Create a sorting key by taking the value of the `position` field at each level leading to each leaf binding object. If `position` is not specified, it is not added to the sorting key. For bindings on arrays and maps, the sorting key must include the array index or map key following the position. If and only if two bindings have the same sort key, the tie must be broken using the ordering of the field or parameter name immediately containing the leaf binding.

つまり、
- 基本は `position` (省略時は 0)
- `position` が同じ場合にはフィールド名

この条件に従ってキーを対応付けたものが以下になります。

|キー | フィールド名 |
|---|---|
|`[2, "reference"]`|`reference` |
|`[3, "reads"]`|`reads`|
|`[1, "minimum_seed_length"]`|`minimum_seed_length`|
|`[1, "min_std_max_min"]`|`min_std_max_min`|
|`[-1, "args.py"]`|`args.py`|

## 4. いい感じにソートする。 
`arguments` と `inputs` での `CommandLineBinding` オブジェクトをソート用キーと一緒にまとめたものが以下の表です。

|キー|`CommandLineBinding` オブジェクト|
|---|---|
|`[0, 0]`| `"bwa"`|
|`[0, 1]`| `"mem"`|
|`[1, 2]`| `{ "valueFrom": ...}`|
|`[2, "reference"]`|`reference` の `inputBinding` |
|`[3, "reads"]`|`reads` の `inputBinding`|
|`[1, "minimum_seed_length"]`|`minimum_seed_length` の `inputBinding`|
|`[1, "min_std_max_min"]`|`min_std_max_min` の `inputBinding`|
|`[-1, "args.py"]`|`args.py` の `inputBinding`|

[4.1 節](https://www.commonwl.org/v1.0/CommandLineTool.html#Input_binding) の 4 の
> Sort elements using the assigned sorting keys. Numeric entries sort before strings.

に従いソートすることで、以下の表が得られます。

|キー|`CommandLineBinding` オブジェクト|
|---|---|
|`[-1, "args.py"]`|`args.py` の `inputBinding`|
|`[0, 0]`| `"bwa"`|
|`[0, 1]`| `"mem"`|
|`[1, 2]`| `{ "valueFrom": ...}`|
|`[1, "min_std_max_min"]`|`min_std_max_min` の `inputBinding`|
|`[1, "minimum_seed_length"]`|`minimum_seed_length` の `inputBinding`|
|`[2, "reference"]`|`reference` の `inputBinding` |
|`[3, "reads"]`|`reads` の `inputBinding`|

## 5. 各 `CommandLineBinding` オブジェクトに対して、引数生成ルールを適用する。

あとは[4.1 節](https://www.commonwl.org/v1.0/CommandLineTool.html#Input_binding) の 5 にあるように、

> In the sorted order, apply the rules defined in `CommandLineBinding` to convert bindings to actual command line elements.

を実行します。

5 中の `rules` は、[5.1.2 節](https://www.commonwl.org/v1.0/CommandLineTool.html#CommandLineBinding)を参照してください。
以下では各 `CommandLineBinding` オブジェクトを、コマンドライン引数に変換していきます。

### `inputs` の `args.py`

```yaml:v1.0/bwa-mem-tool.cwl
inputs:
  ...
  - id: args.py
    type: File
    default:
      class: File
      location: args.py
    inputBinding:
      position: -1
  ...
```

[仕様](https://www.commonwl.org/v1.0/CommandLineTool.html#CommandLineBinding)には以下のようにあるため、単に `path` フィールドの値を使えばいいようです。

> `File`: Add `prefix` and the value of `File.path` to the command line.

しかし与えられたパラメータには `location` フィールドしかありません。
ここで[`File` オブジェクトの仕様 (5.1.5 節)](https://www.commonwl.org/v1.0/CommandLineTool.html#File)を確認すると、`path` については次のように書かれています。

> Alternately to `location`, implementations must also accept the `path` property on File, which must be a filesystem path available on the same host ...

今回は `location` もローカルのファイルパスを指しているため、単に `File.location` の値(つまり `"args.py"`)に変換できます。

### `arguments` 内の `"bwa"`、`"mem"`
[仕様](https://www.commonwl.org/v1.0/CommandLineTool.html#CommandLineBinding)には、

> `string: Add prefix and the string to the command line.

とあるため、それぞれ `"bwa"` と `"mem"` に変換できます。

### `arguments` 内のややこしそうな `CommandLineBinding`オブジェクト

```yaml:v1.0/bwa-mem-tool.cwl
arguments:
  ...
  - valueFrom: $(runtime.cores)
    position: 1
    prefix: -t
```

[5.1.2 節](https://www.commonwl.org/v1.0/CommandLineTool.html#CommandLineBinding) を見ると、
> For binding objects listed in `CommandLineTool.arguments`, the term "value" refers to the effective value after evaluating `valueFrom`.

とあるので、まず `valueFrom` を評価する必要があります。

- `$(...)` については、[3.4 節](https://www.commonwl.org/v1.0/CommandLineTool.html#Parameter_references) および[3.5 節](https://www.commonwl.org/v1.0/CommandLineTool.html#Expressions)に記載されています。これは Expression と呼ばれるもので、実行時のパラメータ (入力オブジェクトや出力ディレクトリ)などに評価される場合(3.4 節)と、JavaScript のコードとして評価される場合(3.5 節)があります。今回は `InlineJavascriptRequirement` は使われていないので、3.4 節のみを参照すればいいです。
- `runtime` が取りうるフィールドについては [4.2 節](https://www.commonwl.org/v1.0/CommandLineTool.html#Runtime_environment)を参照すればよいです。以下を見ると、`$(runtime.cores)` を CPU コア数に評価すれば良いことがわかります。
  - 本文を見ると、`See ResourceRequirement for details on how to describe the hardware resources required by a tool.` とも書いてあるため、今回は `ResourceRequirement` の項も見る必要があります。

> `runtime.cores`: number of CPU cores reserved for the tool process

- [5.12 節](https://www.commonwl.org/v1.0/CommandLineTool.html#ResourceRequirement)に `ResourceRequirement` についての記述があります。今回は `coresMin: 2` と指定があり、また 5.12 節に `If "min" is specified but "max" is not, then "max" == "min"` と記述があるため、最終的に `$(runtime.cores)` が `2` に評価される**ことがあります**。
 
```yaml:v1.0/bwa-mem-tool.cwl
hints:
  - class: ResourceRequirement
    coresMin: 2
```

- 注目すべきは `hints` です。[3.3 節](https://www.commonwl.org/v1.0/CommandLineTool.html#Requirements_and_hints) を見ると、`A hint is similar to a requirement; however, it is not an error if an implementation cannot satisfy all hints.` とあります。つまり実行エンジンは、この `ResourceRequirement` を無視して、`$(runtime.cores)` で 2 以外の値を返しても仕様上は問題ありません。今回は `$(runtime.cores)` が 2 を返す場合を考えます。

[5.1.2 節](https://www.commonwl.org/v1.0/CommandLineTool.html#CommandLineBinding) に数値の変換規則が

> `number`: Add `prefix` and decimal representation to command line.

とあるので、最終的に `["-t", 2]` に変換できます[^2]。

[^2]: が、先程も述べたように `$(runtime.cores)` は2以外にも解釈されうるので、`["-t", 3]` や `["-t", 4]` でも仕様上は間違っていません。しかし `run_tesh.sh` コマンドは `$(runtime.cores)` が `2` にならないとテストが通らないため、実は conformance test #1 は特定の処理系に依存したテストになってしまっています([報告済み](https://github.com/common-workflow-language/common-workflow-language/issues/807))。皆さんも環境や処理系に依存したテストは書かないように気をつけましょうね！

### `inputs` の `min_std_max_min`
```yaml:v1.0/bwa-mem-tool.cwl
inputs:
  ...
  - id: min_std_max_min
    type: { type: array, items: int }
    inputBinding:
      position: 1
      prefix: -I
      itemSeparator: ","
  ...
```

```json:v1.0/bwa-mem-job.json
{
    ...
    "min_std_max_min": [
        1,
        2,
        3,
        4
    ],
    ...
}
```

[仕様](https://www.commonwl.org/v1.0/CommandLineTool.html#CommandLineBinding)には以下のようにあります。`["-I", "1,2,3,4"]` に変換すればよいです。
> `number`: Add `prefix` and decimal representation to command line.
...
> `array`: If `itemSeparator` is specified, add `prefix` and the join the array into a single string with `itemSeparator` separating the items. ...

### `minimum_seed_length`
```yaml:v1.0/bwa-mem-tool.cwl
inputs:
  ...
  - id: minimum_seed_length
    type: int
    inputBinding: { position: 1, prefix: -m }
  ...
```

```json:v1.0/bwa-mem-job.json
{
    ...
    "minimum_seed_length": 3
}
```

[仕様](https://www.commonwl.org/v1.0/CommandLineTool.html#CommandLineBinding)には以下のようにあります。`["-m", 3]` に変換すればよいです。
> `number`: Add `prefix` and decimal representation to command line.

### `inputs` の `reference`

```yaml:v1.0/bwa-mem-tool.cwl
inputs:
  - id: reference
    type: File
    inputBinding: { position: 2 }
```

```json:v1.0/bwa-mem-job.json
{
    "reference": {
        "class": "File",
        "location": "chr20.fa",
        ...
    },
    ...
}
```
[仕様](https://www.commonwl.org/v1.0/CommandLineTool.html#CommandLineBinding)には以下のように書いてあるので、`"chr20.fa"`に変換できます。

> `File`: Add `prefix` and the value of `File.path` to the command line.

### `inputs` の `reads`

```yaml:v1.0/bwa-mem-tool.cwl
inputs:
  ...
  - id: reads
    type:
      type: array
      items: File
    inputBinding: { position: 3 }
  ...
```

```json:v1.0/bwa-mem-job.json
{
    ...
    "reads": [
        {
            "class": "File",
            "location": "example_human_Illumina.pe_1.fastq"
        },
        {
            "class": "File",
            "location": "example_human_Illumina.pe_2.fastq"
        }
    ],
    ...
}
```

[仕様](https://www.commonwl.org/v1.0/CommandLineTool.html#CommandLineBinding)には以下のようにあります。今回は `itemSeparator` も `prefix` も指定されていないので、単に `["example_human_Illumina.pe_1.fastq", "example_human_Illumina.pe_2.fastq"]` を返せばいいです。

> `File`: Add `prefix` and the value of `File.path` to the command line.
...
> `array`: If `itemSeparator` is specified, ... Otherwise first add `prefix`, then recursively process individual elements.


最後に変換したものをまとめると、以下のようになります。

```yaml:
  - "args.py"
  - "bwa"
  - "mem"
  - ["-t", 2]
  - ["-I", "1,2,3,4"]
  - ["-m", 3]
  - "chr20.fa"
  - ["example_human_Illumina.pe_1.fastq", "example_human_Illumina.pe_2.fastq"]
```

## 6. 先頭に `baseCommand` を挿入して完成！

> Insert elements from `baseCommand` at the beginning of the command line.

```yaml:v1.0/bwa-mem-tool.cwl
baseCommand: python
```

先程変換したものに追加して、実行コマンドの完成です！

```console
$ python args.py bwa mem -t 2 -I 1,2,3,4 -m 3 chr20.fa example_human_Illumina.pe_1.fastq example_human_Illumina.pe_2.fastq
```

# 標準出力の捕捉
与えられたツール定義に `stdout` や `stderr` フィールドがある場合、コマンドの標準出力や標準エラー出力をファイルとしてリダイレクトする必要があります ([5 節](https://www.commonwl.org/v1.0/CommandLineTool.html#CommandLineTool))。

```yaml:v1.0/bwa-mem-tool.cwl
stdout: output.sam
```

今回の場合、標準出力の結果が `output.sam` にリダイレクトされます。
また、その時の出力ディレクトリは `$(runtime.outdir)` で指定されたディレクトリです(`cwltool` の場合、デフォルトは `$PWD` で、`--outdir` でも設定できます)。

例えば、出力ディレクトリが `$PWD` の場合、リダイレクトも含めた最終的な実行コマンドは以下になります。

```console
$ python args.py bwa mem -t 2 -I 1,2,3,4 -m 3 chr20.fa example_human_Illumina.pe_1.fastq example_human_Illumina.pe_2.fastq > $PWD/output.sam
```


# 動くか確認してみる

```console
$ pwd
/Users/tom-tan/common-workflow-language
$ cd v1.0/1.0
$ python args.py bwa mem -t 2 -I 1,2,3,4 -m 3 chr20.fa example_human_Illumina.pe_1.fastq example_human_Illumina.pe_2.fastq > $PWD/output.sam
$ ls output.sam
output.sam
```

うまく動いてるようですね！

# これで全部？
実はまだすべきことがあります。
例えば、

```console
$ pwd
/Users/tom-tan/common-workflow-language/v1.0/v1.0
$ git status
...
Untracked files:
...
        cwl.output.json
...
$ cat cwl.output.json
{"args": ["bwa", "mem", "-t", "2", "-I", "1,2,3,4", "-m", "3", "chr20.fa", "example_human_Illumina.pe_1.fastq", "example_human_Illumina.pe_2.fastq"]}
```
先程のコマンドは `output.sam` 以外にもファイルを出力していますが、これは一体どう扱えばいいのでしょうか？

また、実行コマンド生成に使っていないパーツが残っているようです。

```yaml:v1.0/bwa-mem-tool.cwl
...
hints:
  ...
  - class: DockerRequirement
    dockerPull: python:2-slim
...
outputs:
  - id: sam
    type: ["null", File]
    outputBinding: { glob: output.sam }
  - id: args
    type:
      type: array
      items: string
```

具体的には、

- コマンドを Docker コンテナで動かすときに注意すべきことは？ (実装者視点)
- 出力オブジェクトのキャプチャーどうするの
- 頑張って解説した出力オブジェクトのキャプチャルールを台無しにする `cwl.output.json`

# つづく
[次回](https://qiita.com/tm_tn/items/a32064f01769bb05f45d)で全部解説できるはず！