検索/低レベルのデバッガーを、テストを先に書き、ほかのOSを壊さず直す

低レベルのデバッガーを、テストを先に書き、ほかのOSを壊さず直す

多くのOSとバックエンドで動く、コンパイル言語向けのデバッガーをAIに直させる時に使う。先に失敗するテストを書かせ、OSごとのコードを別のファイルに分け、共有インターフェースを変える時は全バックエンドを確かめさせ、コミットメッセージをサブシステム名から始めさせる。

このAIルールを使うには
## デバッガーを直す前に、その動きを受け持つ層を読む コンパイル言語向けのデバッガーは、複雑な多層システムである。その中で効果的に作業するには、デバッグ内部処理、バイナリのデバッグ情報フォーマット、OS固有のプロセス制御、言語ランタイム内部への理解が必要となる。 すべての変更を、生の速度よりもプラットフォームやバックエンド間の正しさが重視されるシステムに触れる行為として扱うこと。メモリアクセス、レジスタ処理、バックエンドの挙動における些細なミスは、特定のプラットフォームでのみ再現しにくい形で表面化する。 編集前に、変更しようとしている振る舞いをどの抽象化層が所有しているかを理解するため、周辺のレイヤーを読むこと。共有インターフェースへの変更は、それを実装するすべてのバックエンドへ波及するため、着手前に影響範囲を見積もること。 ## コードを変える前に失敗するテストを書き、テストを飛ばさない すべてのコード変更はテスト駆動開発に従わなければならない。これはオプションではない。 Red-Green-Refactor-Verifyサイクルに従うこと。 1. **RED** - 最初に失敗するテストを書く。正しい理由で失敗しなければならない。 2. **GREEN** - それを通すために必要最小限のコードを書く。 3. **REFACTOR** - テストをグリーンに保ちながら整理する。 4. **VERIFY** - コミット前にテストスイート全体を実行する。 必須ルール: 1. 失敗するテストを先に書かずに実装コードを書いてはならない。 2. テストなしでコードをコミットしてはならない。 3. CIを通すためにテストをスキップしたり、スキップ扱いにしたりしてはならない。 4. 既存のテストを無効化してはならない - テストかコードのどちらかを直すこと。 5. 各テストは1つの具体的な振る舞いだけを検証する。 6. テスト名はテスト対象のシナリオを説明する。 テストを先に書くことが難しく感じるとき、その難しさは言い訳ではなくシグナルである。多くの場合、関数が多くのことをやりすぎている、依存関係が密結合になっている、あるいは要件が不明確であることを意味する。TDDを飛ばすのではなく、そこで露呈した設計上の問題を直すこと。 ## テストの種類ごとの置き場所を決め、毎回同じ結果にする テストの置き場所: - **ユニットテスト**:対象コードと同じ場所に配置する。 - **統合テスト**:専用のエンドツーエンドテストディレクトリに配置する。 - **プラットフォーム固有のテスト**:ビルドタグ/条件付きコンパイルでゲートする。 - **バックエンド固有のテスト**:各バックエンドを個別に検証する。 - **フィクスチャ**:ビルド済みバイナリではなく、テスト実行時にコンパイルされるソースとして保持する。パス探索を自前で組むのではなく、共有のフィクスチャロケーターヘルパーを使う。 品質基準: - 実装ではなく振る舞いをテストする。 - 1テストにつき1アサーションを優先し、失敗が1つの原因を指し示すようにする。 - テスト対象・シナリオ・期待される結果をアンダースコア区切りで表すパターンで、明確に命名する。 - 既存のテストユーティリティを再利用し、車輪の再発明をしない。 - テストは決定的でなければならない - ランダム値や競合状態を含めない。 - 出力フォーマットのテストでは、部分文字列ではなく期待される出力全体に対してアサートする。完全一致による比較は、余分な空白・フィールドの並び替え・フォーマット変更といった、部分文字列チェックでは見逃されがちな回帰を捕捉する。 ## 読み取り専用と状態変更用のインターフェースを、層の間で分ける このシステムは、関心事が明確に分離されたレイヤーとして構成されている:最上位にCLI、その下にサービス/プロトコル層、さらにその下に高レベル操作、その下にプロセス/ターゲット抽象化、そして最下層にプラガブルなバックエンド実装(OSレベルの直接プロセス制御、リモートプロトコルクライアント、オフラインダンプ解析、カーネルレベルのトレーシング)が置かれる。 抽象化境界では2階層インターフェース設計を用いる: - 呼び出し側が状態を観測するために使う、読み取り専用のパブリックインターフェース。 - 状態を変更するすべての操作を担う、別個の内部インターフェース。 - 内部インターフェースを包むステートフルなラッパーオブジェクトと、複数のターゲットを同時に管理するためのグループ抽象化。 バックエンドは内部インターフェースの背後にプラグ可能な形で存在する。このインターフェースを変更すると、その変更は通常すべての実装に適用されるため、目の前のバックエンドだけでなく、すべてのバックエンドを考慮すること。 ## OSごと・アーキテクチャごとのコードを別のファイルに分ける OS固有・アーキテクチャ固有のコードは、ファイル名ベースの条件付きコンパイルで分離する - OSごとのソースファイルとアーキテクチャごとのソースファイルを、実行時分岐ではなくビルドシステムによって選択する。 - プラットフォーム固有のコードを汎用ファイルに入れてはならない。 - プラットフォーム固有のコードは、それを実際に必要とするバックエンド(OS直接制御バックエンドとリモートプロトコルバックエンド)に限定し、共有のコアロジックには置かない。 - 多数のアーキテクチャをサポートする場合、レジスタ・ABI・呼び出し規約の詳細はアーキテクチャごとのファイルに保持する。 うまく行えば、共有コアは移植性を保ったままとなり、新しいプラットフォームの追加は汎用コードを書き直す作業ではなく、ファイルを追加する作業になる。 ## プロセスのメモリ読み取りとブレークポイントで、よくあるミスを避ける 1. **共有状態を直接変更しない** - ロックと状態の一貫性を提供する内部インターフェースのメソッドを経由すること。 2. **バックエンド固有の操作は異なる** - ある1つの概念的な操作(例:ブレークポイントの挿入)は、バックエンドごとに異なる方法で実装される。単一の仕組みを前提にしてはならない。 3. **類似する抽象化を区別する** - OSスレッドと言語レベルの並行処理単位は同じものではない。ある操作がどちらを対象にしているかを把握すること。ほとんどの操作は生のスレッドではなく、言語レベルの単位に対して作用する。 4. **メモリ安全性** - ターゲットプロセスのメモリを読み取る際は、常にエラーを処理すること。プロセスが終了する場合や、メモリがアンマップされている場合、アドレスが無効である場合がある。 5. **冗長なパスを追加しない** - 既存のパスがすでに全ユニット(コンパイル単位、シンボルなど)を走査しているなら、別途スキャンするのではなく、そのパスにユニットごとのチェックを追加すること。 6. **プロローグの後に計装を配置する** - ブレークポイントやプローブは、生のエントリポイントではなく関数プロローグの後に設定する。エントリポイントで計装すると、ランタイムのスタックチェック用プロローグが再トリガーされることがあるためである。 ## コミットメッセージを、サブシステム名から始めて変えた理由を書く サブシステムベースのコミット形式を使用する:影響を受けるサブシステム名の後にコロンと変更内容の短い要約を続けた件名行、続けて空行、続けて変更を行った理由を説明する本文の段落、続けて修正対象のissueを参照する締めの行、という構成である。 - 履歴を領域ごとに一覧しやすくするため、件名の先頭に影響を受けるサブシステムを付ける。 - 件名行は簡潔にし、上限(およそ70文字)内に収める。 - 本文では変更を行った理由を説明し、(およそ80文字で)折り返す。 - 各変更がその原因となったissueにひも付くよう、動機となったissueを参照する。 構造化されたメッセージは変更履歴を追いやすく保ち、ある変更がなぜ行われたかを後から再構築しやすくする。