マニュアルテストのエビデンス取得効率化話【vol.2】

株式会社オズビジョンのユッコ (@terra_yucco) です。

以前に**[マニュアルテストのエビデンス取得効率化話【vol.1】](https://qiita.com/drafts/b6144bad61091283ff93)**というエントリを書きましたが、本日、そのとき頂いたフィードバックも元にもう少し別のものを作ったので【vol.2】として晒してみます。
※なお、この記事を【vol.2】としたので、以前の記事は本日付でリネームして【vol.1】としています。

# エビデンス取得用スクリプト【vol.2】
## 要件

- そのテストがいつ行われたものか知りたい
- そのテストで実行されたコマンドが知りたい
- 事前事後で比較する必要があるものは自動的に取得しておきたい
    - DB 内容
    - ls 結果 など
- 同じような動きをするスクリプトをまとめてテストしたい

## 与件 (制約)

テスト対象は PHP で書かれたバッチ処理ですが、多重起動を抑制するために sh ラッパーからコールしています。sh ラッパー側で flock を使って多重起動を抑制するという仕組み。

```sh:download_A.sh
#!/bin/sh
flock --timeout=0 /path/to/lock/download_A /usr/bin/php /path/to/git/htdocs/index.php download_A index
```

また 1 つのスクリプトの中で 2 本の PHP を呼ぶケースもあります。

```sh:download_M.sh
#!/bin/sh
flock --timeout=0 /path/to/lock/download_M_1 /usr/bin/php /path/to/git/htdocs/index.php download_M_1 index
flock --timeout=0 /path/to/lock/download_M_2 /usr/bin/php /path/to/git/htdocs/index.php download_M_2 index
```

これを local vagrant 内でテストしたいのですが、vagrant がうまく構築できておらず、以下の問題があります。

- flock の ver が異なっており timeout=0 がエラー
- php のパスが異なっており `/usr/bin/php` で起動不可
    - ただし `php` で起動可能

ひとつひとつは対応していけばいいのですが、今回は以下のような方針としました。

## 実装方針：sh スクリプトから php コマンドを抜き出してあれやこれやする
### 0. 実コード

今回は複数のバッチのエンドポイントになっている controller に影響する abstract の修正だったので、テスト対象のスクリプトが起動 sh ベースで 13 本ありました。
ただし動作は簡単なもので、外部の指定された URL へ接続し、機能に対応した所定のディレクトリ (なければ作成) にファイルを保存するというものなので定型化できました。

一つ前の記事で @tenmyo さんよりご指摘いただいた、

>ファイル名同じにして、ディレクトリ名タイムスタンプにすると、WinMergeの比較やりやすいですよ。

というのをここでは採用させていただきました。今回は git の branch を切り替えて、修正前のコードと修正後のコードを流したので、ディレクトリごと比較することで大半の比較を一瞬で終わらせることが出来ました。多謝。

```bash:ev2.sh
#!/bin/bash -u

base_dir=/path/to/git
batch_dir=${base_dir}/batch
data_dir=${base_dir}/data/hoge

log_path=/tmp/*.log
modified_class_path=${base_dir}/applications/fuga/controllers/test_abstract.php

batches=(
  ${batch_dir}/download_A.sh
  ${batch_dir}/download_B.sh
  ${batch_dir}/download_C.sh
  ${batch_dir}/download_D.sh
  ${batch_dir}/download_E.sh
  ${batch_dir}/download_F.sh
  ${batch_dir}/download_G.sh
  ${batch_dir}/download_H.sh
  ${batch_dir}/download_I.sh
  ${batch_dir}/download_J.sh
  ${batch_dir}/download_K.sh
  ${batch_dir}/download_L.sh
  ${batch_dir}/download_M.sh
)

set '-x'
date
rm -f ${log_path}
ls -l ${modified_class_path}
cat ${modified_class_path}

cd ${data_dir}
pwd
rm -fr *
for batch in ${batches[@]}; do
    echo "***** ${batch} *****"
    cmds=$( grep index.php ${batch} | sed -e 's/^.*\/php/php/' )
    ORG_IFS=${IFS}
    IFS='
'
    for cmd in ${cmds[@]}; do
        date
        pwd
        ls -lR
        ls -l ${log_path}
        ${cmd}
        ls -lR
        ls -l ${log_path}
        date
    done
    IFS=${ORG_IFS}
done

dirname=${base_dir}/$(date +%Y%m%d%H%M%S )
mkdir ${dirname}
mv ${data_dir}/* ${dirname}
mv ${log_path} ${dirname}

ls -l ${modified_class_path}
cat ${modified_class_path}

exit 0
```

以下、コードの箇所を順に解説していきます。

### 1. php の起動行取得

```bash
cmds=$( grep index.php ${batch} | sed -e 's/^.*\/php/php/' )
```

PHP の routing は htdocs/index.php を通るようになっているので、それを利用し、

```sh
#!/bin/sh
flock --timeout=0 /path/to/lock/download_M_1 /usr/bin/php /path/to/git/htdocs/index.php download_M_1 index
flock --timeout=0 /path/to/lock/download_M_2 /usr/bin/php /path/to/git/htdocs/index.php download_M_2 index
```

から以下のようにする処理。

```sh
php /path/to/git/htdocs/index.php download_M_1 index
php /path/to/git/htdocs/index.php download_M_2 index
```

sed は最長一致なので、単純に `php` で引っかけると失敗します。
また、肯定先読みが使えない (`?` をメタ文字扱いしない) ので、冗長に見える書き方をしています。

### 2. IFS の変更

```bash
ORG_IFS=${IFS}
    IFS='
'
```

`IFS` は環境変数で、文字の区切りとして認識する文字のことです。デフォルトでは `sp(0x20)` `ht(0x09)` `nl(0x0a)` が設定されています。

```terminal
$  echo "${IFS}" | od -ax
0000000  sp  ht  nl  nl
           0920    0a0a
0000004
```

今回は、デフォルトの `IFS` を一回退避し、デフォルトのうち `nl(0x0a), \n` のみを残しています。
これはこの後 for 文でコマンドを 1 行ずつ拾いたいときに `IFS` がデフォルトだとコマンドもぶった切られたためです。

```terminal
+ cmds='php /path/to/git/htdocs/index.php download_M_1 index
php /path/to/git/htdocs/index.php download_M_2 index'
```

`-x` によるトレースが上記のようになっている場合、
`for cmd in ${cmds[@]}` の `$cmd` に入るものとして

- `IFS` デフォルト
    - `php`
    - `/path/to/git/htdocs/index.php`
    - `download_M_1`
    - `index`
    - `php`
    - `/path/to/git/htdocs/index.php`
    - `download_M_2`
    - `index`
- `IFS` が `\n` のみ
    - `php /path/to/git/htdocs/index.php download_M_1 index`
    - `php /path/to/git/htdocs/index.php download_M_2 index`

となります。

### 2b. (別解) read の利用

この記事を書きながら思いましたが、

```bash
    cmds=$( grep index.php $batch | sed -e 's/^.*\/php/php/' )
    ORG_IFS=${IFS}
    IFS='
'
    for cmd in ${cmds[@]}; do
```

この部分を以下のように変更すれば `IFS` の変更はしなくても同じように動きます。
`IFS` は戻し忘れるとその後想定外の挙動をするので、こちらの方がいいコードかもしれません。

```bash
    cmds=$( grep index.php $batch | sed -e 's/^.*\/php/php/' )
    echo "${cmds}" | while read cmd; do
```

### おまけ

抽出したコマンドを実際に実行する

```bash
        ${cmd}
```

の箇所は、誤ってこれを `"${cmd}"` のようにクォートしてしまうと以下のようになってコマンドを起動できません。

```terminal
+ 'php /path/to/git/htdocs/index.php download_M_1 index'
/path/to/ev2.sh: 行 47: php /path/to/git/htdocs/index.php download_M_1 index: そのようなファイルやディレクトリはありません
```

今回はクォートを外せば済む話ですが、何かでクォートされたものからコマンドを起動しないといけない場合には

```bash
        eval "${cmd}"
```

のようにすればコマンドとして起動することができます。

#### サンプルコードと実行例

```bash:sample.sh
#!/bin/bash -u
set '-x'
cmd="date +%Y%m%d%H%M%S"
${cmd}
"${cmd}"
eval "${cmd}"
```

```terminal
$  bash -u sample.sh
+ cmd='date +%Y%m%d%H%M%S'
+ date +%Y%m%d%H%M%S
20190524213153
+ 'date +%Y%m%d%H%M%S'
sample.sh: 行 5: date +%Y%m%d%H%M%S: コマンドが見つかりません
+ eval 'date +%Y%m%d%H%M%S'
++ date +%Y%m%d%H%M%S
20190524213153
```

## Refined code

```bash:ev2_refined.sh
#!/bin/bash -u

base_dir=/path/to/git
batch_dir=${base_dir}/batch
data_dir=${base_dir}/data/hoge

log_path=/tmp/*.log
modified_class_path=${base_dir}/applications/fuga/controllers/test_abstract.php

batches=(
  ${batch_dir}/download_A.sh
  ${batch_dir}/download_B.sh
  ${batch_dir}/download_C.sh
  ${batch_dir}/download_D.sh
  ${batch_dir}/download_E.sh
  ${batch_dir}/download_F.sh
  ${batch_dir}/download_G.sh
  ${batch_dir}/download_H.sh
  ${batch_dir}/download_I.sh
  ${batch_dir}/download_J.sh
  ${batch_dir}/download_K.sh
  ${batch_dir}/download_L.sh
  ${batch_dir}/download_M.sh
)

set '-x'
date
rm -f ${log_path}
ls -l ${modified_class_path}
cat ${modified_class_path}

cd ${data_dir}
pwd
rm -fr *
for batch in ${batches[@]}; do
    echo "***** ${batch} *****"
    cmds=$( grep index.php ${batch} | sed -e 's/^.*\/php/php/' )
    echo "${cmds}" | while read cmd; do
        date
        pwd
        ls -lR
        ls -l ${log_path}
        ${cmd}
        ls -lR
        ls -l ${log_path}
        date
    done
done

dirname=${base_dir}/$(date +%Y%m%d%H%M%S )
mkdir ${dirname}
mv ${data_dir}/* ${dirname}
mv ${log_path} ${dirname}

ls -l ${modified_class_path}
cat ${modified_class_path}

exit 0
```

## Conclusion

PHP もそうなのですが、テストコードやこういうテスト用ツールを作っているときのほうがいろいろなことをやっているような気がします。
今回はバッチ起動用の sh スクリプトから PHP の実行コマンドを切り出し、その事前事後やタイムスタンプを取得することでエビデンス比較が楽になる自作スクリプトを紹介しました。
プロダクト開発は PHP でも LAMP 環境である以上 Linux 上での作業はなくならないもの。そういう意味でも Bash は便利です。