「赤にならない」の前に、欠陥が本当に入ったかを数える
テストの強さを確かめる一番確実な方法は、わざと壊して赤を見ることだ。
守りたかった状態を壊し、テストが落ちることを確認して初めて、そのテストは「効いている」と言える。
問題はその次にある。壊したのに赤にならなかったとき、何を疑うか。
同じ観測に、正反対の原因が 2 つある
3 本のガードを注入検証したところ、1 本だけ緑のままだった。
一瞬、「このテストは弱い、書き直そう」と思った。間違いだった。
実際には、注入に使ったシェルコマンドの引用が壊れていて、置換が 1 文字も起きていなかった。
ファイルは元のまま。テストが緑なのは当たり前で、テストはむしろ正しく振る舞っていた。
「赤が出ない」には原因が 2 つある。
- テストが弱い ⇒ 直すべきはテスト
- 欠陥が入っていない ⇒ 直すべきは注入手順
この 2 つは観測が完全に同じなのに、対処が正反対だ。
区別しないまま進むと、効いているテストを「弱い」と決めつけて書き直すという、
最悪の方向へ手が動く。書き直した先のテストが本当に強いかは、また同じ手順で確かめることになる。
判定の前に「注入できたこと」を数える
対処はごく単純で、置換件数を印字してから判定する。
before = s.count(target)
s = s.replace(target, broken)
open(p, 'w').write(s)
after = open(p).read().count(target)
print('%d → %d (0 なら欠陥が入った)' % (before, after))
1 → 0 を目で見てから、はじめてテストを回す。今回は 1 → 1 のままだったので、
置換し直したら即座に赤になった。テストは最初から効いていた。
- 置換が 0 件ならエラーで止める。 黙って素通りさせない。
置換ヘルパは一致件数が期待と違ったら sys.exit(1) にしておく
- 復元したあとは
git diffが空であることまで見る。
注入検証は「壊す・確かめる・戻す」の 3 段で、戻し漏れも同じくらい静かに起きる
これは「検出器が母集団を見ていないのに 0 件で緑になる」問題とまったく同じ形だ。
0 件は合格ではなく「測れていない」であり、件数を印字しない手順は信じられない。
注入検証にとっての母集団は、置換対象が何箇所あったかである。
壊れたのはコードではなく、引用だった
注入が失敗した直接の原因も書いておく。python3 -c "..." に、
クォートと改行とバックスラッシュの混ざった置換文字列を渡していた。
シェルが先に展開してしまい、Python に届いた時点で別の文字列になっていた。
⇒ 複雑な引用をシェル経由で渡さない。 ヒアドキュメントで渡す。
python3 - <<'PY'
target = "if (!isAdmin(user)) return deny();"
...
PY
終端子を 'PY' とクォートすると、シェルは中身を一切展開しない。<<PY とクォート無しで書くと変数やコマンド置換が展開されるので、
クォートの有無だけで結果が変わる。ここも「気づけないと静かに間違う」側の分岐だ。
まとめ
- 注入して赤にならなかったら、テストを疑う前に注入を疑う
- 判定の前に置換件数(
1 → 0)を印字し、0 件ならエラーで止める - 復元後に
git diffが空であることまで確認する - 注入スクリプトはヒアドキュメントで渡す。シェルの引用は静かに壊れる