Conformance test #1 (Docker & 出力パラメータ編)

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

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

# 注意！

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

# つづき

昨日の記事では conformance test #1 で記述されたツールの実行コマンドの生成について解説しました。
本日は、昨日説明していなかった `DockerRequirement` と出力オブジェクトの捕捉ルールについて解説します。

# 昨日のおさらい

これと

```yaml:v1.0/bwa-mem-tool.cwl
#!/usr/bin/env cwl-runner

cwlVersion: v1.0

class: CommandLineTool

hints:
  - class: ResourceRequirement
    coresMin: 2
  - class: DockerRequirement
    dockerPull: python:2-slim

inputs:
  - id: reference
    type: File
    inputBinding: { position: 2 }

  - id: reads
    type:
      type: array
      items: File
    inputBinding: { position: 3 }

  - id: minimum_seed_length
    type: int
    inputBinding: { position: 1, prefix: -m }

  - id: min_std_max_min
    type: { type: array, items: int }
    inputBinding:
      position: 1
      prefix: -I
      itemSeparator: ","

  - id: args.py
    type: File
    default:
      class: File
      location: args.py
    inputBinding:
      position: -1

outputs:
  - id: sam
    type: ["null", File]
    outputBinding: { glob: output.sam }
  - id: args
    type:
      type: array
      items: string

baseCommand: python

arguments:
  - bwa
  - mem
  - valueFrom: $(runtime.cores)
    position: 1
    prefix: -t

stdout: output.sam
```

これが

```json:v1.0/bwa-mem-job.json
{
    "reference": {
        "class": "File",
        "location": "chr20.fa",
        "size": 123,
        "checksum": "sha1$hash"
    },
    "reads": [
        {
            "class": "File",
            "location": "example_human_Illumina.pe_1.fastq"
        },
        {
            "class": "File",
            "location": "example_human_Illumina.pe_2.fastq"
        }
    ],
    "min_std_max_min": [
        1,
        2,
        3,
        4
    ],
    "minimum_seed_length": 3
}
```

こうなった。

```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
$ 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"]}
```

その結果、以下が余った。

```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
...
```

# `DockerRequirement`
[3.3 節](https://www.commonwl.org/v1.0/CommandLineTool.html#Requirements_and_hints)に記載があるように、`hints` にあるものは無視しても仕様上は問題無いため、実行コマンドは前回の最終版でも問題ありません。今回は `DockerRequirement` をちゃんと処理する場合を考えます。

```yaml:v1.0/bwa-mem-tool.cwl
hints:
  ...
  - class: DockerRequirement
    dockerPull: python:2-slim
```

仕様の[5.5 節](https://www.commonwl.org/v1.0/CommandLineTool.html#DockerRequirement)に以下の記述があります。

> The platform must execute the tool in the container using `docker run` with the appropriate Docker image and tool command line.
> The workflow platform may provide input files and the designated output directory through the use of volume bind mounts.

Docker 上でコマンドを実行するために、以下の処理が必要です。

- 各引数の `File` オブジェクトを volume mount します。
    - `File` オブジェクトから生成された引数は、`args.py`、`chr20.fa`、`example_human_Illumina.pe_1.fastq`、`example_human_Illumina.pe_2.fastq` の4種類です。それぞれ `-v` で、docker コンテナ内からアクセスできるようにします。

```console
$ docker run -it --rm -v $PWD/v1.0/args.py:/staged/args.py:ro -v $PWD/v1.0/chr20.fa:/staged/chr20.fa:ro -v $PWD/v1.0/example_human_Illumina.pe_1.fastq:/staged/example_human_Illumina.pe_1.fastq:ro -v $PWD/v1.0/example_human_Illumina.pe_2.fastq:/staged/example_human_Illumina.pe_2.fastq:ro python:2-slim ...
```

- 最初に示したように、`args.py` はカレントディレクトリに `cwl.output.json` を出力します。`docker run` 内で出力したファイルは、何もしなければ虚空の彼方に消えてしまうので、出力ディレクトリを volume mount する必要があります。[4.2 節](https://www.commonwl.org/v1.0/CommandLineTool.html#Runtime_environment)を見ると、出力ディレクトリは `HOME` として参照できないといけないため、`-e` オプションを用いて `HOME` を特定のディレクトリ(docker 内の場合は適当なディレクトリで問題ありません)を指すようにします。また、`HOME` で指定したディレクトリを volume mount で docker 外から参照できるようにします。最後に、`--workdir` でワーキングディレクトリを指定します。今回は `-e HOME=/output -v $PWD:/output:rw --workdir=/output` を追加して、これらを行えるようにします。

```console
$ pwd
/Users/tom-tan/common-workflow-language/v1.0
$ docker run -it --rm -e HOME=/output -v $PWD:/output:rw --workdir=/output -v $PWD/v1.0/args.py:/staged/args.py:ro -v $PWD/v1.0/chr20.fa:/staged/chr20.fa:ro -v $PWD/v1.0/example_human_Illumina.pe_1.fastq:/staged/example_human_Illumina.pe_1.fastq:ro -v $PWD/v1.0/example_human_Illumina.pe_2.fastq:/staged/example_human_Illumina.pe_2.fastq:ro python:2-slim python /staged/args.py bwa mem -t 2 -I 1,2,3,4 -m 3 /staged/chr20.fa /staged/example_human_Illumina.pe_1.fastq /staged/example_human_Illumina.pe_2.fastq > $PWD/output.sam
```


# 出力オブジェクトの補足

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

上を見ると、`sam` と `args` の二種類の出力オブジェクトがあることがわかります。

`sam` の型の `["null", File]` については、[Issue 605](https://github.com/common-workflow-language/common-workflow-language/issues/605) を確認することで、「`"null"` 型か `File` 型のどちらかを取る型」(Issue 中では union type)であることがわかります。
[5.2.3 節](https://www.commonwl.org/v1.0/CommandLineTool.html#CommandOutputBinding)を参照すると、`outputBinding.glob` で指定したファイル名 `output.sam` がある場合には、その `output.sam` を表す `File` オブジェクトが返ることがわかります[^1]。今回はコマンドの標準出力が `output.sam` にリダイレクトされるので、このファイルを表す `File` オブジェクトが(その中身の有無にかかわらず) `sam` として返る…

[^1]: conformance test #1 の意図としては、`output.sam` がない場合には `"null"` 型の唯一の値である `null` が返る気がしますが、その点についてはどこにも記載がありません…

**と思うけどそうはならないのが conformance test #1!**


# 出力オブジェクトの説明をガン無視する `cwl.output.json` に関する仕様
仕様をよく読むと、[4.4 節](https://www.commonwl.org/v1.0/CommandLineTool.html#Output_binding)に `cwl.output.json` に関する記述があります。曰く:

> If the output directory contains a file named "cwl.output.json", that file must be loaded and used as the output object. 

つまり、

```console
$ 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"]}
```

この内容がこのまま出力オブジェクトになります。**`glob` をどう解釈するとかこのテストには全く関係ありません。**

最終的に出力オブジェクトは、`cwl.output.json` の中身である

```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"
  ]
}
```

となります。値が `null` になっているフィールドは省略できる[^2]ため、`sam` フィールドは出力オブジェクトには現れません。

[^2]: これも仕様に無い([報告済み](https://github.com/common-workflow-language/common-workflow-language/issues/744))

# まとめ

大変でしたね！
CWL でツール定義を書く人や、他の人から CWL ファイルを受け取って実行する人は、上記を気にしなくてもツールやワークフローの実行が可能です。全て処理系が善きにはからってくれます。

処理系を作る人は頑張りましょう！

…個人的には、最初は単純な ExpressionTool (#14 や #15) から始めるのがとっつきやすいと思います。