ログに残らないバグを、絵と座標の経路を辿って解決する
先生が画面で見るカードと学生が受け取るPNGは、別々のコードが描いています。 片方にだけ修正が入ると、エラーなしで静かに違う絵が届きます。偽のキャンバスで2つの経路の座標を記録して比べるテストを作りました。
発音矯正サービスのフィードバックカードは2回描かれます。先生が画面で添削するときはリアルタイムでレンダリングコードがキャンバスに描かれ、学生に届けるときは別のexportコードがPNGとして出力されます。2つのコードは同じ717×423の座標系を使うので、散布図の点と曲線の座標は完全に一致しなければなりません。
先生は自分の画面の曲線の上に、描き込みと動画で添削します。学生が受け取るPNGの曲線が少しでも違う位置にある場合、添削が指している場所と実際の曲線がずれたカードが届いてしまいます。
実際に一度起きました。ピッチ補正のロジックを修正した際に片方のレンダリング経路にだけ反映され、曲線が点の群れから外れたカードが出ていきました。別の事件もあります。音節の文字をライブ画面はHTMLで、exportはキャンバスで描くのですが、export側の文字描画が抜けて、文字のないカードが保存されたこともありました。
2つの事件の共通点は、どのログにも残らなかったことです。例外は発生せず、レスポンスコードも正常で、モニタリングの指標も動きませんでした。アプリから見れば、絵が少し違うだけですべて成功です。この種のバグは人の目に留まるまで、存在そのものが分かりません。
そこで、2つのレンダリング経路が同じ座標を描いているかを自動で突き合わせるテストを作りました。方法は、描画コマンドを横取りする偽のキャンバスコンテキストです。
const createRecorder = (): Recorder => {
const arcs: Arc[] = []
const beziers: Bezier[] = []
const moveTos: Array<{ x: number; y: number }> = []
const texts: Text[] = []
const ctx = {
// 座標を記録するもの
arc: (x: number, y: number, r: number) => arcs.push({ x, y, r }),
bezierCurveTo: (...args: number[]) => beziers.push(args),
moveTo: (x: number, y: number) => moveTos.push({ x, y }),
// ...
}
}本物のキャンバスの代わりにこのrecorderをレンダリング関数とexport関数にそれぞれ渡すと、2つの関数がどの点(arc)と曲線(bezier)をどこに描こうとしたかが配列として残ります。テストはその配列が互いに完全に一致することを検証します。ピクセルを比べるスナップショットテストと違ってレンダリング環境が不要で、ずれたらどの点が何px違うのか座標ですぐ分かります。
文字も記録します。音節の文字はexport経路だけがキャンバスに描くので、これを記録しないと先ほどの「文字が抜けたカード」を捕まえられないからです。
テストはずれを捕まえてくれますが、そもそもずれようがない構造の方が良い。そこでピッチ補正のようなデータ加工は、2つのレンダリング経路が分かれる前に一度だけ行います。オクターブエラーの折りたたみもこの位置にあり、レンダリングとexportが同じ矯正結果を受け取ります。
それでも片方のレンダリングだけを直すミスは起こりえます。そうしたずれは座標突き合わせテストで引っかかります。
この方法はキャンバス専用ではありません。画面とPDF、サーバー計算とクライアント計算のように、同じ結果を出すべきコードが2つあればどこでも使えます。成果物の比較が難しければ、成果物を作るコマンドや中間値を記録して突き合わせれば良く、2つの経路が一致するかだけを見るので期待値を別に用意する必要もありません。