zudo-test-wisdom
GitHub リポジトリ

検索したい単語を入力

いつでも検索バーを開ける

Rustスイートをcargoからnextestへ移行する

このガイドのリトライ予算・テストごとのタイムアウト・flakyテレメトリのポリシーが、プレーンな cargo test では実装不可能である理由と、Rustスイートをcargo-nextestへ移行するための機械的なチェックリスト。

なぜnextestがRustのflakeパイプラインのゲートなのか

重いテストの判断ルール実行ティアは、このガイドのすべてのCIスイートに同じ3つの義務を課します:1〜2回のリトライ予算、トリアージのシグナルとして記録されるpass-on-retry、そして1つのテストがハングしてもジョブ全体をハングさせない、テストごとのタイムアウトです。Rustプロジェクトでは、これらはプレーンな cargo test の上では単に扱いづらいだけではなく — 実装不可能です。cargo test にはリトライの仕組みも、テストごとのタイムアウトも、安定した機械可読な出力もありません。cargo-nextest はその3つすべてを提供します。だからこそRustのflakeパイプラインは、組み込みのハーネスではなくnextestを通して動くのです。

Policy requirement (this guide)plain cargo testcargo nextest
CI retry budget 1–2noneretries = 2
Pass-on-retry recorded as triage signalinvisiblereported as FLAKY, in JUnit
Per-test timeout (no hung CI jobs)none (job-level only)slow-timeout + terminate-after
Machine-readable output for issue filingnone stableJUnit XML

リトライ予算は retries に、pass-on-retryのトリアージシグナルはnextestのFLAKYレポートとJUnit出力に、テストごとのタイムアウトは slow-timeout + terminate-after に、issue起票のための機械可読なフィードはJUnit XMLに対応します。このガイドの各ポリシー行には、直接対応するnextestの仕組みがあり、cargo test には等価物がありません。

設定のスケッチ

ポリシーは2つのファイルが担います。nextestのプロファイルがリトライ予算・テストごとのタイムアウト・JUnitテレメトリをエンコードし、さらにプレーンな cargo test がスイートに無償で与えていた直列化を再確立します:

# .config/nextest.toml
[profile.ci]
retries = 2                                              # the CI retry budget (1–2)
slow-timeout = { period = "60s", terminate-after = 4 }   # per-test timeout

[profile.ci.junit]
path = "junit.xml"                                       # flake telemetry / deduped issue filing

# Preserve the serialization the suite silently relied on under cargo test:
[test-groups]
heavy-serial = { max-threads = 1 }

[[profile.default.overrides]]
# every integration-test binary that boots a dev server / binds ports / embeds V8
filter = 'binary(dev_serve_e2e) | binary(dev_build_static_parity) | binary(dev_serve_injected_routes_e2e)'
test-group = 'heavy-serial'

CIステップは cargo testcargo nextest run に置き換えます — ただしnextestはdoctestを実行しません。そのため、doctestのステップは明示的に残す必要があります:

# CI
- uses: taiki-e/install-action@nextest   # cargo nextest is not part of the default toolchain — install it first
- run: cargo nextest run --workspace --profile ci
- run: cargo test --doc --workspace   # nextest does not run doctests — drop this step and doctest coverage silently disappears

Warning

cargo test --doc のステップを削ることは、静かなカバレッジの喪失です。 nextestはdoctestを一切実行しません。移行が cargo testcargo nextest run に置き換えるだけで他に何もしなければ、すべてのdoctestが実行されなくなり、それでもスイートはグリーンになります。doctestの実行は独立したステップとして残してください。

移行チェックリスト(機械的)

  1. まずdoctestのサーフェスを計測する — フェンスを分類する(実行可能な rust と、rustdocが決して実行しない texttshtmljson)。実際の数を移行PRに記録する。cargo test --doc --workspace は独立したステップとして残す。(言い伝えの数は、たいてい計測した数よりはるかに多いものです。)

  2. 暗黙の直列化を列挙するcargo test はテストバイナリを順番に実行しますが、nextestはバイナリをまたいで並列化します。ポートにバインドする/サーバを起動する/重量級ランタイムを埋め込む、すべての統合テストバイナリをリストアップし、最初のflake stormの後ではなく、最初の並列実行の前max-threads = 1 のテストグループに入れる。防御を厚くするため、共有のtest-utilsクレートにアドバイザリな flock を追加してもよい。

  3. コメント中の並行性の主張を監査する — 主張されている仕組み(flock、ロックファイル、mutex)をgrepし、信頼する前にそれが実際に存在することを確認する。

  4. インベントリを突き合わせる — バイナリごとに cargo test -- --listcargo nextest list を差分し、すべての差分をPRで説明する。静かな欠落(nextestが異なる基準でフィルタするテスト)が主要な失敗モードです — より少ないテストを実行するグリーンな移行PRは、移行しないより悪いのです。

  5. 隔離の相互運用 — 既存の #[ignore] 隔離ノートはきれいに対応します:T3の失敗許容ジョブは cargo nextest run --run-ignored ignored-only(任意で名前フィルタ、現在のノートと同じ注意点)になります。

Note

ステップ5は、このレシピを重いテストの判断ルールの隔離パイプラインへ結び付けます:そこで説明されている #[ignore] ベースの隔離は、nextestの下では cargo nextest run --run-ignored ignored-only として実行され、同じ名前フィルタの注意点が当てはまります。

タグ分類の実行

重いテストの判断ルール § 同じルール、Rust構文の理由文字列タグ分類は、あるテストがなぜ #[ignore] を持つのかを分類します。このセクションは、nextestがその分類を実際の実行レーンへどう変換するかを扱います。

--run-ignored:隔離/エグザムレーン

cargo nextest run --workspace --profile ci                              # T1: the PR gate -- never runs an #[ignore]'d test, whatever its reason
cargo nextest run --workspace --profile exam --run-ignored ignored-only # T3: the quarantine/exam lane -- runs ONLY the ignored set
cargo nextest run --workspace --profile exam --run-ignored all          # full-inventory sanity run -- everything, ignored and not; not a gate

--run-ignored ignored-only は、定期再試験にあるPlaywrightの --grep "@flaky" 隔離レーンのnextest版です — ただし、こちらは理由文字列の中身ではなく #[ignore]有無で選択します。nextestは引用符の中身を一切パースしません。あの文字列は人間とgrepのためのものであり、nextest自身のフィルタリングのためのものではないのです。heavy:flaky: の理由が混在するignore済みセットを持つワークスペースは、両方を同じ ignored-only の実行にまとめて走らせてしまい、Playwright側が独立した --grep 式で保っていた区別を消してしまいます。両者を別々に判断する必要があるときは、名前フィルタで絞り込みます:

# separate "expected to always pass, just slow" from "expected to sometimes fail"
cargo nextest run --profile exam --run-ignored ignored-only -E 'test(/^heavy_/)'
cargo nextest run --profile exam --run-ignored ignored-only -E 'test(/^flaky_/)'

これは、上の隔離のNoteがすでに flaky: 単体について挙げている注意点を一般化したものです:#[ignore]@flaky に対してだけでなく、分類全体にわたって、どの単一の理由よりも広いマーカーなのです。

test-groups:エグザムレーン内での予算・直列化コントロール

上のConfig Sketchは、ポートにバインドする統合バイナリを再直列化するために test-groups エントリ(heavy-serial)を使っています。エグザムレーンにも同じ仕組みが別の理由で必要です:エグザムレーンは遅くてもよいのですが、無制限ではありません — heavy: でignoreされたセットがランナーのスレッドを丸ごと飽和させてしまうと、「少数の遅いテスト」がエグザムレーン自身の重量級スイート問題に変わってしまいます。

新しいグループは、上のConfig Sketchが開いた既存の [test-groups] テーブルに追加します — 同じファイルに2つ目の [test-groups] ヘッダーを置くのは無効なTOMLです。テーブルを2回宣言することはできません:

# .config/nextest.toml -- inside the [test-groups] table already opened by the Config Sketch
heavy-serial = { max-threads = 1 } # existing, from the Config Sketch above
exam-heavy = { max-threads = 2 }   # new: caps concurrency for `heavy:`-ignored tests specifically

exam プロファイル自体は新規なので、独自のテーブルを持ちます:

# .config/nextest.toml (new tables, appended after the Config Sketch above)
[profile.exam]
retries = 0                        # exam is diagnostic, not gated -- no retry budget to hide a real fail

[profile.exam.junit]
path = "junit.xml"                 # same machine-readable feed the ci profile uses, for the filing script

[[profile.exam.overrides]]
filter = 'test(/^heavy_/)'         # name-convention filter narrowing to the `heavy:` reason
test-group = 'exam-heavy'

専用の [profile.exam] が必要なのは — ci プロファイルに --run-ignored を後付けするのではなく — エグザムレーンのリトライ・スレッド化のデフォルトがPRゲートのそれとは正当に異なるからです:T1は本物のリトライ予算を求めます(リトライ予算を参照)。エグザムレーンが求めるのはゼロです。そうすることでパスは曖昧さがなく、失敗は本物になります。

失敗許容のスケジュール実行エグザムジョブ

このスケジュールジョブは、定期再試験 § flakyテレメトリのメカニズムにあるPlaywright隔離レーンの終了コードキャプチャパターンをそのまま踏襲します:continue-on-error: trueif: failure() の組み合わせがあちらでフィリングステップを死んだコードにしてしまうのと同じ理由で、こちらでも同じことが起こります — ステップレベルの continue-on-error はそのステップの conclusion をsuccessに書き換えてしまうため、下流の if: failure() は決して発火しません。

# .github/workflows/exam.yml
name: exam

on:
  schedule:
    - cron: "51 3 * * *"  # off-minute on purpose -- see Scheduled Re-exam
  workflow_dispatch:

permissions:
  contents: read
  issues: write

jobs:
  exam:
    runs-on: ubuntu-latest
    timeout-minutes: 60
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: taiki-e/install-action@nextest

      - name: Run the quarantine/exam lane (allowed-to-fail BY DESIGN)
        id: exam
        run: |
          set +e
          cargo nextest run --workspace --profile exam --run-ignored ignored-only
          rc=$?
          set -e
          echo "rc=$rc" >> "$GITHUB_OUTPUT"
          # Deliberate: this lane is green-by-design. The captured rc -- not the
          # job status -- drives the filing step below.
          exit 0

      - name: File or update the tracking issue
        if: steps.exam.outputs.rc != '0'
        env:
          GH_TOKEN: ${{ github.token }}
        run: bash scripts/file-exam-issue.sh

file-exam-issue.sh は、定期再試験にある1ワークフロー1トラッキングissueパターンと同じものです — ラベルとタイトルによる重複排除、コメントか新規作成かの判定、グリーン時のクローズというロジックはそのまま移植できます。異なるのは失敗抽出のステップだけです:Playwrightの JSON レポーターの代わりに、上の [profile.exam.junit] が生成する junit.xml をパースします。

次に読むページ

  • 重いテストの判断ルール — このnextest設定が実行する #[ignore] 理由文字列タグ分類の全体(env-gate:heavy:flaky:verification:pending-feature:)と、Rustのflakeがどう隔離されるか

  • 実行ティア — この移行がRustスイート上で実装可能にするために存在する、リトライ予算

  • フレイク根本原因カタログ & デフレイキングレシピ — nextestがバイナリをまたいで並列化した瞬間に表面化する、ネイティブスイートのflake原因(ポートレース、暗黙の直列化、共有状態)

Revision History

作成更新