フィールド番号を変えたら、古いクライアントが黙って間違った値を読んだ
契約検査器を作る
目標
2つの.protoを比較して、ワイヤーの互換性違反を1行ずつ出力するprotocheck.pyを
作ります。ルールを1つずつ追加しながら、フィクスチャ8組と採点ツールが隠している組を通し、
最後にはディレクトリ単位で実行するcheck-all.shを作って、CIに入れられる
形に仕上げます。
なぜ重要なのか
前のラボで、番号を1つ変えると、旧クライアントがエラーなしで誤った値を読むことを 確認しました。その変更はコードレビューでは目立ちません。diffには数字が2つ 変わっただけで、コンパイルもテストも通ります。人が毎回見つけられる種類の ミスではありません。そのため、契約のチェックはレビュアーではなくスクリプトが行う必要があります。 チェッカーのルールはすべて、公式ドキュメントの「安全な変更・安全でない変更」の一覧から 導かれており、あなたが作るのは、その一覧を機械が読める形に書き写したものです。
ステップ
/root/grpc/compat/protocheck.pyを作成し、python3 protocheck.py --dump <file.proto>が、メッセージごとのフィールドを<메시지> <번호> <타입> <이름>の形式で、予約を<메시지> reserved <번호>と<메시지> reserved "<이름>"の形式で、1行ずつ出力するようにしてください(プレースホルダーはメッセージ名、番号、型、名前です)。コメント(//、/* */)・空白・オプション([deprecated=true])は無視し、9 to 11は3つの番号に展開し、ネストしたメッセージはOuter.Innerと呼びます。python3 protocheck.py <old.proto> <new.proto>に最初のルールを入れてください。古い番号が新しいファイルになく、reservedでもなければREMOVED_NOT_RESERVED <메시지>.<이름>=<번호>です(プレースホルダーはメッセージ名、名前、番号です)。違反があればexit 1、なければ最後の行にOKを出力してexit 0です。- 同じ名前が別の番号に移っていれば、
RENUMBERED <메시지>.<이름> <옛번호>-><새번호>です(プレースホルダーはメッセージ名、名前、古い番号、新しい番号です)。 - 同じ番号・同じ名前で型が違えば
TYPE_CHANGED <메시지>.<이름>=<번호> <옛타입>-><새타입>(プレースホルダーはメッセージ名、名前、番号、古い型、新しい型です)、同じ番号で名前も型も違えばNUMBER_REUSED <메시지>#<번호> <옛이름>:<옛타입>-><새이름>:<새타입>です(プレースホルダーはメッセージ名、番号、古い名前と型、新しい名前と型です)。 - 古いファイルがreservedにした番号(範囲を含む)を、新しいファイルがフィールドとして使えば
REUSED_RESERVED <메시지>#<번호> <이름>です(プレースホルダーはメッセージ名、番号、名前です)。 - 同じ番号・同じ型で名前だけが違えば、
WARN RENAMED <메시지>#<번호> <옛이름>-><새이름>を出力しますが、終了コードには影響を与えないでください(警告だけならOK・exit 0)(プレースホルダーはメッセージ名、番号、古い名前、新しい名前です)。 /opt/app/grpc/compat/の8組をすべて実行して、/root/grpc/compat/07-report.txtに<case> OKまたは<case> FAILを1行ずつ書いてください。/root/grpc/compat/check-all.sh <디렉터리>を書いてください(プレースホルダーはディレクトリです)。その下でold.protoとnew.protoがある子ディレクトリごとにチェッカーを実行し、<case>: OK/<case>: FAILを出力して、FAILが1つでもあればexit 1とします。
参考
- ルールの原本: proto3 — メッセージ型の更新、Protoのベストプラクティス。
- フィクスチャ:
safe_addremove_no_reservedrenumbertype_changereuse_reservedrename_onlynestedsafe_reserved。各ディレクトリのnew.protoのコメントに、何を変更したかが書かれています。 - 採点ツールは、フィクスチャ以外にも指示文にない.protoの組を一時的に作って、あなたのスクリプトに渡します。フィクスチャ名や結果を埋め込むと、その場で不合格になります。判定は終了コードと出力だけで行います。
- ルールの優先順位: 名前が新しいファイルのどこかで別の番号になっていれば、RENUMBEREDとして報告し、その番号はそれ以上見ません。番号が新しいファイルに残っていれば、REMOVEDではありません。
- パーサーは、前のラボの採点ツールのように寛容でなければなりません。
syntax = "proto3" ;も読めなければなりません。
.protoを読むパーサーを作る
/root/grpc/compat/protocheck.pyを作成し、--dump <file.proto>がフィールドを<메시지> <번호> <타입> <이름>の形式で、予約を<메시지> reserved <번호> / <메시지> reserved "<이름>"の形式で、1行ずつ出力するようにしてください(プレースホルダーはメッセージ名、番号、型、名前です)。
まずコメントを消し(re.subを2回)、message 이름 {(プレースホルダーはメッセージ名です)を探して中括弧を数えながら対応を取り、その内側を読みます。内側にさらにmessageがあれば再帰しますが、名前はOuter.Innerと付けます。フィールドは타입 이름 = 번호;(プレースホルダーは型、名前、番号です)の正規表現1つで捕まえられ、先頭のoptional/repeatedは捨てます。
reserved 9 to 11, 15;はカンマで分けた後、toがあればrangeで展開します。reserved "foo";は名前の予約です。
採点ツールは、フィクスチャ以外にも、コメント・変わった空白・範囲の予約・ネスト・オプションが混ざったファイルを渡して試します。
予約なしで削除された番号を捕まえる
python3 protocheck.py <old> <new>に、REMOVED_NOT_RESERVED <메시지>.<이름>=<번호>のルールを入れてください(プレースホルダーはメッセージ名、名前、番号です)。違反があればexit 1、なければ最後の行OK・exit 0です。
メッセージごとに、古いファイルの番号を1つずつ見て、新しいファイルにあるか、なければ新しいファイルのreservedにあるかを確認します。どちらでもなければ違反です。remove_no_reservedとnested(Customer.tier)が不合格になり、safe_add・safe_reservedはOKになるはずです。
終了コード: sys.exit(1)は違反が1つでもあるときだけで、なければprint("OK")の後に0です。
番号が移ったものを捕まえる
RENUMBERED <메시지>.<이름> <옛번호>-><새번호>のルールを追加してください(プレースホルダーはメッセージ名、名前、古い番号、新しい番号です)。
新しいファイルで、名前 → 番号の辞書を先に作れば、1行で終わります。古い名前がその辞書にあり、番号が違えば違反です。この場合、その番号に対する他のチェック(REMOVEDなど)はスキップします。1つの原因に1行が望ましいからです。
renumberフィクスチャでは、idとqtyの2行が出力されるはずです。
型の変更と番号の再利用を捕まえる
TYPE_CHANGED <메시지>.<이름>=<번호> <옛타입>-><새타입>とNUMBER_REUSED <메시지>#<번호> <옛이름>:<옛타입>-><새이름>:<새타입>のルールを追加してください(プレースホルダーはメッセージ名、名前、番号、古い型、新しい型、古い名前、新しい名前です)。
同じ番号が両方にあって型が違う場合、名前が同じならTYPE_CHANGED、名前も違えばNUMBER_REUSEDです。番号が新しいファイルに残っているので、REMOVEDではありません。
ネストしたメッセージ(Order.Item)の中の型の変更も捕まえられなければなりません。パーサーがOuter.Innerと名前を付けていれば、メッセージごとの比較がそのまま通用します。
予約された番号を取り出して使ったものを捕まえる
REUSED_RESERVED <메시지>#<번호> <이름>のルールを追加してください(プレースホルダーはメッセージ名、番号、名前です)。古いファイルのreserved範囲(10 to 12)も対象に含みます。
新しいファイルのフィールド番号が、古いファイルのreserved集合にあれば違反です。ステップ1で範囲を展開しておけば、in1つで終わります。reuse_reservedフィクスチャと、採点ツールが作るreserved 10 to 12+memo = 11の組が不合格になるはずで、同じファイルで13を使うのはOKです。
名前の変更は警告だけ
同じ番号・同じ型で名前だけが違えば、WARN RENAMED <메시지>#<번호> <옛이름>-><새이름>を出力しますが、終了コードには影響を与えないでください(プレースホルダーはメッセージ名、番号、古い名前、新しい名前です)。警告だけならOK・exit 0です。
警告とエラーを別のリストに集め、終了コードはエラーのリストだけを見ます。警告はエラーより先に出力し、エラーがなければ最後にOKを出力します。rename_onlyはWARN1行+OK、採点ツールが作る「名前の変更+型の変更」の組は、WARNとTYPE_CHANGEDが一緒に出力されてexit 1になります。
フィクスチャ8組をすべて実行する
/opt/app/grpc/compat/の8組をすべて実行して、/root/grpc/compat/07-report.txtに<case> OKまたは<case> FAILを1行ずつ書いてください。
for d in /opt/app/grpc/compat/*/; do ...; doneで実行し、終了コードでOK/FAILを決めます。手で書かないでください。採点ツールは、あなたのスクリプトをもう一度実行してレポートと突き合わせ、本当の答えとも突き合わせます。3つがすべて一致して合格です。
ディレクトリ単位のチェックスクリプト
/root/grpc/compat/check-all.sh <디렉터리>を書いてください(プレースホルダーはディレクトリです)。old.protoとnew.protoがある子ディレクトリごとにチェッカーを実行し、<case>: OK / <case>: FAILを出力して、FAILが1つでもあればexit 1としてください。
引数として受け取ったディレクトリを巡回しますが、2つのファイルが両方ある子ディレクトリだけを対象にします(ファイルや空のディレクトリはスキップします)。失敗の数を数えて、最後にexitします。採点ツールは、フィクスチャのディレクトリ以外にも、名前の違う一時ディレクトリを2つ作って実行します。パスを埋め込むと不合格になります。
protocheck.pyの場所は、このスクリプトがあるディレクトリを基準にすれば($(dirname "$0"))、どこから呼び出しても動きます。