下流テスト
ライブラリ開発者は、Hyrumの法則により明示的なAPIだけでなく暗黙の依存関係も考慮する必要があり、DebianやFedoraのダウンストリームテスト導入がその解決策として注目されている。
キーポイント
Hyrumの法則とセマンティックバージョニングの限界
実際の使用は依存コードにあり、公式ドキュメントやテストではカバーされない暗黙の契約が存在する。Mavenの調査では、非メジャーバージョンでも11.58%が破壊的変更を含んでおり、従来のバージョニングシステムではこれを防げない。
Debianのautopkgtestによる事前検知
Debianはパッケージがテスト環境へ移行する際、逆依存関係を持つパッケージのテストを実行する。これにより、上位互換性を壊す変更がユーザーに届く前に検知され、修正が可能になる。
FedoraのPR段階でのダウンストリームテスト
Cockpitプロジェクトは、Pull Request作成時に依存パッケージのテストを自動実行する仕組みを導入した。これにより、リリース後のバグ報告待ちではなく、変更直後に修正コストを最小限に抑えられる。
ダウンストリームテストのタイミングとコスト
変更直後に発見された失敗は修正コストが低いが、リリース後に報告されるとコンテキストの再構築や対応に大きな時間がかかる。
ディストリビューションと言語エコシステムの違い
ディストリビューションは単一の依存関係グラフや標準化されたテストインターフェースを持つが、npmやPyPIなどの言語エコシステムはツールが断片化しており、公開を制限する仕組みがない。
動的言語におけるダウンストリームテストの重要性
Rustのようなコンパイル型言語はビルド時にエラーが検出されやすいが、PythonやJavaScriptなどの動的言語では実行時エラーしか検出されないため、ダウンストリームテストの価値が高い。
既存の信頼性指標は維持者にフィードバックされない
RenovateやDependabotが提供するMerge Confidenceや互換性スコアは、実際のテスト結果に基づく有用なシグナルではあるが、そのデータはパッケージ維持者へ返却されず、非公開または有料の壁の中に留まっている。
影響分析・編集コメントを表示
影響分析
この記事は、ソフトウェア開発における「後手後手の修正」から「予防的な検証」へのパラダイムシフトを示唆しています。特に大規模なオープンソースエコシステムでは、単体テストだけでなく依存関係全体を考慮した統合テスト(Downstream Testing)をCI/CDパイプラインに組み込むことが、メンテナンス負担の軽減と信頼性向上に不可欠であることを示しています。
編集コメント
Karpathy氏によるこの指摘は、AIモデルの出力検証や大規模な機械学習パイプラインにおける依存関係管理にも通じる重要な教訓です。モデルの挙動を「ブラックボックス」として扱うのではなく、その出力が downstream のシステムにどう影響するかを事前にシミュレートする文化の重要性を示しています。
ライブラリが実際にどのように使用されているかという情報は、ライブラリ自身のテストやドキュメントではなく、依存する側のコードに存在します。誰かがあなたのエラーメッセージを正規表現で解析していたり、あなたが文書化していない結果セットの反復順序に依存していたり、可視性を強制しない言語でプライベートとマークされていないため内部メソッドだと考えているメソッドに依存していたりする可能性があります。ヒュームの法則は、十分な数のユーザーがいる場合、これらの暗黙的な契約がすべて存在すると述べており、セマンティックバージョニング(semver)では対応できません。なぜならバージョン番号はメンテナーの意図を示すものであり、下流のコードが実際に依存しているものを示すものではないからです。2023 年の Maven に関する研究では、依存関係の更新の 11.58% にクライアントに影響を与える破壊的変更が含まれており、そのほぼ半分が非メジャーバージョンのアップグレードで発生していました。ほとんどのライブラリメンテナーには公開前にバージョン番号を検証する方法がないため、フィードバックループは反応的になります:リリースしてバグレポートを待ち、パッチを切る前に破壊的な影響が広範囲に及んでいないことを願うのです。
Debian パッケージは DEP-8 仕様に従ってテストスイートを宣言しており、パッケージが不安定版から安定版への移行候補となった際、移行ツール Britney はそのパッケージおよびすべての逆依存関係に対して autopkgtest をトリガーします。回帰現象は移行をブロックするため、依存先でテスト失敗を引き起こす Expat の更新は、誰かがそれを解決するまで不安定版に留まります。同様に、mathcomp-analysis や mathcomp-finmap を壊した Coq の更新も同じ扱いを受けました。変更が不安定版への参加を希望しないユーザーに到達する前に、メンテナーは誰をどのように壊したかを確認します。
Autopkgtest は API 互換性をチェックしません。それは実際の消費者の実際のテストスイートを実行し、そこにはアップストリームメンテナーが聞いたこともないような暗黙的な契約も含まれています。ライブラリ Y がパッチリリースでハッシュテーブルのソート順序を変更し、パッケージ X のテストがその順序が安定していると仮定していた場合、誰かがどちらの仮定が間違っているかを決定するまで移行はブロックされます。
Fedora は最近、tmt、Packit、Testing Farm との連携により、リリース前に PR 内で下流テストを実行する取り組みを進めています。Cockpit プロジェクトはこの仕組みを構成しており、コアライブラリへの PR を開くと、提案された変更に対して cockpit-podman やその他の依存プロジェクトのテストスイートが自動的に実行され、マージ前のステータスチェックとして結果が表示されます。彼らが述べているように、「そもそもディストリレベルでは手遅れです。その時点では、回帰を含む新しいアップストリームリリースはすでに完了しており、原因となった変更は数週間前に既に導入されている可能性が高いのです」。
メンテナが PR 内で不具合を発見した場合、まだ変更の範囲内にいます。なぜエラーパスを再構築したのかを覚えており、どのテストを検討したかも知っており、差分も目の前にあります。この時点で下流での失敗に対応するコストは、数分の思考と、おそらく修正されたアプローチで済みます。一方、同じ不具合がリリースから 3 週間後に問題として報告された場合、メンテナは変更の文脈を再読込みし、なぜ壊れたのかを理解するために下流プロジェクトの使用状況を十分に把握する必要があり、前方修正すべきかリバートすべきかを決定し、新しいリリースを作成し、すでに固定バージョンを使用している消費者がアンピン(固定解除)してくれることを願うしかありません。両方のケースで得られる情報は同じです。「下流テストに失敗した」という事実ですが、それに対応するコストは、原因となった変更からの距離に応じて増大します。
Debian の autopkgtest は、テスト環境への移行前に不具合を検出します。これは事後に検出するより優れていますが、その時点ですでに変更はアップストリームでリリース済みです。一方、Fedora のアプローチでは、アップストリームのリリース自体が発生する前に不具合を検出するため、メンテナーは自分たちの CI 環境外の誰かが遭遇する前に修正できます。František Lachman と Cristian Le は FOSDEM で PTE プロジェクトを発表しました。コード変更を執筆している最中に届くダウンストリームからのフィードバックは、その変更自体に対する考え方を根本から変えます。
言語エコシステム
ディストリビューションがこれを実現できるのは、言語エコシステムに欠けている構造的な特性を持っているからです:単一の正統な依存関係グラフ、標準化されたテストインターフェース(Debian の場合は DEP-8)、すべてのパッケージが同じ方法でビルドされ実行される共有実行環境、そしてダウンストリームの結果に基づいてリリースをブロックする権限です。npm、PyPI、RubyGems はツールが断片化しており、リポジトリ外からパッケージのテストを呼び出す標準的な方法がなく、実行環境も多様であり、メンテナー自身の判断以外の何かに基づいて公開を制限する仕組みを持っていません。一部の言語エコシステムではダウンストリームテストの部分的な実装が進んでいますが、これらは通常、これらのギャップを埋めるリソースを持つコンパイラチームに属しています。
Rust の crater は、crates.io 上のすべてのクレートを現在のコンパイラと提案中のコンパイラの両方でコンパイル・テストし、結果を差分比較します。最近の PR で f32 に対する impl From が追加されました
Crater は Rust がコンパイルされる点からも恩恵を受けています。型推論の失敗は、テストを実行する前のビルド時点で検出されます。一方、Python、Ruby、JavaScript では、同様の不具合が表面化するのはランタイム時であり、そのため影響を受けるコードパスを実際に実行するダウンストリームテストスイートが必要となります。さらに、それらのコードパス自体が最初にカバされている必要があります。コンパイルステップが存在せず、簡単なミスを検出できないため、動的なエコシステムではダウンストリームテストの必要性はより強く、シグナルを取得することも困難になります。
Node.js は CITGM(Canary in the Goldmine)を実行しており、提案された Node のバージョンに対して約 80 の厳選された npm パッケージをテストします。Node 12 でのリファクタリングにより isFile メソッドが Stats.prototype から StatsBase.prototype へ移動しました。
これらすべては、専用のインフラストラクチャ予算とリリースプロセスを持つチームによって構築されました。一方、npm や PyPI、RubyGems で広く使用されるパッケージを公開する個々のライブラリメンテナーには、同等の体制はありません。彼らは異なる規模で同じ問題に直面しているにもかかわらずです。
マージ自信度
Renovate の Merge Confidence は、数百万件の更新 PR からデータを集約し、消費者に対して更新が安全かどうかを伝えます:リリースの古さ、Renovate ユーザーのうちどれほどがそれを採用しているか、そして更新結果としてテストに合格する割合などです。このシグナルは実際のプロジェクトにおける実際のテスト結果から得られますが、リリース後に消費者へ流れ、変更を公開したメンテナには決して戻りません。アルゴリズムは非公開であり、どの更新がどのプロジェクトのテストを壊したかという基盤となるデータセットも、Mend の有料壁の向こう側に残されています。
Dependabot はセキュリティ更新 PR に対して互換性スコアを表示しますが、これは同じ更新を行った他のパブリックリポジトリの CI 結果から計算されます。ただし、少なくとも 5 つの候補更新が存在する場合に限られ、データもメンテナへは戻りません。私はこのシグナルのオープン版を構築するために、dependabot.ecosyste.ms で Dependabot の PR をインデックス化し始めました。まだ CI データはありませんが、すでに更新ごとのマージ率を追跡しており、これは特定のバージョンアップがエコシステム全体でどの程度のトラブルを引き起こしているかの大まかな代理指標となっています。
レジストリは、他のパッケージに依存関係を宣言するパッケージを追跡しますが、ライブラリを消費するアプリケーションはほとんど可視化されません。gem に依存する Rails アプリケーションは RubyGems の逆依存関係リストには表示されず、npm パッケージを使用する企業の内部サービスも npmjs.com 上には現れません。依存先に関するメンテナの視点も、レジストリが把握できる範囲に限られており、これは他のライブラリに大きく偏っており、アプリケーションを見逃しています。しかし、ここでこそ、奇妙な使用パターンやより驚くべき暗黙的な契約が現れる場所なのです。
ecosyste.ms は、パッケージとオープンソースリポジトリの両方にわたって依存先を追跡し、GitHub、GitLab、およびその他のフォージ上の数百万のリポジトリをスキャンして、依存関係を宣言するマニフェストファイルを探します。メンテナは、実際に自分のライブラリを使用しているアプリケーションを確認できますが、これは下流テストシステムを構築するために必要な視点です。
これは ecosyste.ms の上に構築したい機能です。メンテナーが CI サービスに接続すると、すべてのプルリクエストやプレリリースブランチにおいて、ecosyste.ms に対してそのパッケージの上位 N 件の依存先(ライブラリとアプリケーションの両方)を照会します。ランク付けは、依存先の数、ダウンロード量、コミットの最新性の組み合わせに基づいて行われます。その後、各依存先をクローンし、現在のリリース版の代わりに提案されたバージョンのライブラリを置き換えた上で、隔離環境内でテストスイートを実行します。結果はプルリクエストへのレポートとして返されます:どの依存先がテストされたか、どの依存先で後退(レグレス)が発生したか、スタックトレースの内容はどのようなものか、メンテナーの変更のうち各失敗の原因となった可能性が高いものはどれか。
リリースタグを付ける前にこのレポートを確認するメンテナーには、現在では自分たちには見えない事象が明らかになります。例えば、人気のあるアプリケーションがエラーメッセージを正規表現でパースしており、文言が変わると壊れてしまうこと、広く使われているラッパーライブラリが内部とみなしていたメソッドを呼び出しており、それが削除されようとしていたこと、データベース呼び出しのバッチ処理に関する最適化によりコールバック順序が変更され、それが 2 つの下游プロジェクトの統合テストに依存していることがわかります。
Michal Gorny が指摘する下流テストにおける Python パッケージの問題カタログは、以下の失敗モードを列挙しています。インストール済みファイルを破棄可能なコンテナ内にあると仮定して変更を試みるテストスイート、環境内の pytest プラグインが予期せぬテスト収集を引き起こす問題、ネットワークアクセスや Docker を必要とするテスト、アーキテクチャ間で異なる浮動小数点精度の差異、テストファイル自体を省略したソースディストリビューションです。レジストリー全体でこれを実行しようとするサービスは、これらのすべてを適切に処理し、本格的な回帰と環境ノイズを区別する必要がありますが、これは Debian が autopkgtest を用いて何年もかけて改良してきたにもかかわらず、まだ完全に解決できていない難しい問題です。
ecosyste.ms はすでに依存関係の発見を提供しており、ソースリポジトリはパッケージメタデータからリンクされています。テストスイートは自動化可能なほど十分に理解されているエコシステム規約に従っており、コンテナインフラストラクチャにより隔離環境を低コストで実現できます。Crater と autopkgtest は、このアプローチがエコシステム規模で機能することを証明しています。
原文を表示
The information about how a library is actually used lives in the dependents’ code, not in the library’s own tests or docs. Someone downstream is parsing your error messages with a regex, or relying on the iteration order of a result set you never documented, or depending on a method you consider internal because it wasn’t marked private in a language that doesn’t enforce visibility. Hyrum’s Law says all of these implicit contracts exist once you have enough users, and semver can’t help because a version number declares what the maintainer intended, not what downstream code actually depends on. A 2023 study of Maven found that 11.58% of dependency updates contain breaking changes that impact clients, and nearly half arrived in non-major version bumps. Most library maintainers have no way to validate their version number before publishing, so the feedback loop is reactive: release, wait for bug reports, and hope the breakage wasn’t too widespread before you can cut a patch.
Debian packages declare test suites following the DEP-8 specification, and when a package is a candidate for migration from unstable to testing, the migration tool Britney triggers autopkgtest for the package and all of its reverse dependencies. A regression blocks migration, so an Expat update that causes test failures in its dependents sits in unstable until someone resolves them, and a Coq update that broke mathcomp-analysis and mathcomp-finmap did the same. The maintainer finds out who they broke and how before the change reaches anyone who didn’t opt into unstable.
Autopkgtest doesn’t check API compatibility. It runs actual test suites of actual consumers, which encode whatever implicit contracts those consumers have built against, including ones the upstream maintainer has never heard of. If library Y changes the sort order of a hash table in a patch release and package X’s tests assumed that order was stable, migration blocks until someone decides whose assumption was wrong.
Fedora’s recent work with tmt, Packit, and Testing Farm runs downstream tests in the PR, before anything is released. The Cockpit project configured it so that opening a PR on their core library automatically runs the test suites of cockpit-podman and other dependents against the proposed change, with results showing up as status checks before merge. As they put it, “it is too late at the distro level anyway: at that point the new upstream release which includes the regression was already done, and the culprit landed possibly weeks ago already.”
When a maintainer discovers breakage in a PR, they’re still inside the change. They remember why they restructured that error path, they know which tests they considered, and the diff is right in front of them. The cost of responding to a downstream failure at this point is a few minutes of thought and maybe a revised approach. When the same breakage surfaces as an issue filed three weeks after release, the maintainer has to reload the context of the change, understand the downstream project’s usage well enough to see why it broke, decide whether to fix forward or revert, cut a new release, and hope that consumers who already pinned away will unpin. The information is the same in both cases, a downstream test failed, but the cost of acting on it scales with the distance from the change that caused it.
Debian’s autopkgtest catches breakage before migration to testing, which is better than catching it after, but the change has already been released upstream by that point. The Fedora approach catches it before the upstream release happens at all, which means the maintainer can fix it before anyone outside their own CI ever encounters it. František Lachman and Cristian Le presented the PTE project at FOSDEM. Downstream feedback that arrives while you’re still writing the code changes how you think about the change itself.
Language ecosystems
Distributions can do this because they have structural properties that language ecosystems lack: a single canonical dependency graph, a standardized test interface (DEP-8 in Debian’s case), a shared execution environment where every package builds and runs the same way, and the authority to block a release based on downstream results. npm, PyPI, and RubyGems have fragmented tooling, no standard way to invoke a package’s tests from outside its own repo, heterogeneous execution environments, and no mechanism to gate a publish on anything other than the maintainer’s own judgement. A few language ecosystems have built partial versions of downstream testing anyway, though they tend to belong to compiler teams with the resources to work around these gaps.
Rust’s crater compiles and tests every crate on crates.io against both the current and proposed compiler, then diffs the results. A recent PR adding impl From<f16> for f32
Crater also benefits from Rust being compiled: a type inference failure shows up at build time, before any tests run. In Python, Ruby, or JavaScript, the equivalent breakage only surfaces at runtime, so you need downstream test suites that actually exercise the affected code paths, and those code paths need to be covered in the first place. The case for downstream testing is stronger in dynamic ecosystems because there’s no compile step to catch the easy ones, and the signal is harder to get.
Node.js runs CITGM (Canary in the Goldmine), which tests about 80 curated npm packages against proposed Node versions. A refactor in Node 12 moved isFile
Stats.prototype
StatsBase.prototype
All of these were built by teams with dedicated infrastructure budgets and release processes, and an individual library maintainer who publishes a widely-used package on npm or PyPI or RubyGems has nothing comparable, even though they face the same problem at a different scale.
Merge confidence
Renovate’s Merge Confidence aggregates data from millions of update PRs to tell consumers whether an update is safe: how old the release is, what percentage of Renovate users have adopted it, and what percentage of updates result in passing tests. The signal comes from real test results across real projects, but it arrives after the release and flows to consumers, never back to the maintainer who shipped the change. The algorithm is private, and the underlying dataset of which updates broke which projects’ tests stays behind Mend’s paywall. Dependabot shows a compatibility score on security update PRs, calculated from CI results across other public repos that made the same update, but only when at least five candidate updates exist, and the data doesn’t flow back to the maintainer either. I’ve started indexing Dependabot PRs at dependabot.ecosyste.ms to build an open version of this signal. It doesn’t have CI data yet, but it already tracks merge percentages per update, which gives a rough proxy for how much trouble a particular version bump is causing across the ecosystem.
Registries track which packages declare dependencies on other packages, but applications that consume libraries are mostly invisible: a Rails app that depends on a gem won’t show up in RubyGems’ reverse dependency list, and a company’s internal service using an npm package won’t appear on npmjs.com. The maintainer’s view of their dependents is limited to whatever the registry can see, which skews heavily toward other libraries and misses the applications, which are where the stranger usage patterns and more surprising implicit contracts show up.
ecosyste.ms tracks dependents across both packages and open source repositories, scanning millions of repos on GitHub, GitLab, and other forges for manifest files that declare dependencies. A maintainer can see which applications actually use their library, which is the view you’d need to build a downstream testing system on.
This is something I want to build on top of ecosyste.ms. A maintainer connects the service to their CI, and on every PR or pre-release branch it queries ecosyste.ms for the top N dependents of the package, both libraries and applications, ranked by some combination of dependent count, download volume, and recency of commits. It clones each one, installs the proposed version of the library in place of the current release, and runs their test suite in an isolated environment. The results come back as a report on the PR: which dependents were tested, which ones regressed, what the stack traces look like, which of the maintainer’s changes likely caused each failure.
A maintainer looking at that report before tagging a release would see things that are currently invisible to them. They’d see that popular applications parse their error messages with regex and will break if the wording changes, that a widely-used wrapper library calls a method they considered internal and were about to remove, that their optimisation to batch database calls changed the callback order in a way that two downstream projects’ integration tests depend on.
Michal Gorny’s catalogue of problems with downstream testing Python packages lays out the failure modes: test suites that modify installed files assuming they’re in a disposable container, pytest plugins in the environment causing unexpected test collection, tests requiring network access or Docker, timing-dependent assertions, floating-point precision differences across architectures, source distributions that omit test files entirely. Any service trying this across a registry would need to handle all of these gracefully, distinguishing genuine regressions from environmental noise, which is a hard problem that Debian has spent years refining with autopkgtest and still hasn’t fully solved.
ecosyste.ms already provides the dependent discovery, source repositories are linked from package metadata, test suites follow ecosystem conventions that are well-understood enough to automate, and container infrastructure makes isolated environments cheap. Crater and autopkgtest have proven the approach works at ecosystem scale.
関連記事
今日のまとめ
AI日報で今日の重要ニュースをまとめ読み