ci: speed up full cross-platform PR validation (#296)

Use pinned nextest for full native tests while retaining doctests, production-feature checks and both macOS release checks.

Publish shared dependency snapshots from successful default-branch runs, keep compiled artifacts architecture-specific, and preserve cross-process exclusion for shared theme fixtures.

Run the full platform matrix for both draft and ready pull requests without repeating it for readiness-only transitions.
This commit is contained in:
Kuddev authored and GitHub committed 2026-09-26 13:25:29 +08:00
1 parent fde914bb50
commit 33e7b1d00d
11 files changed
+368 -84

No files matched your search

+8
View File
@@ -0,0 +1,8 @@
# 这些现有夹具共享真实设置文件;进程内 Mutex 在 nextest 多进程模型下不再互斥。
# 只还原该夹具组的串行合同,其余测试继续使用 runner 的并行度。
[test-groups]
theme-studio = { max-threads = 1 }
[[profile.default.overrides]]
filter = 'test(gpui_shell::settings_pane::theme_studio_tests::)'
test-group = 'theme-studio'
+37 -10
View File
@@ -14,6 +14,12 @@ inputs:
extra-key:
description: Additional inputs for native build tools stored with this target
default: ""
revision:
description: Snapshot suffix; empty uses the source SHA, dependency snapshots use a fixed generation
default: ""
save-if:
description: Allow publishing caches; PR consumers can restore without competing with the default branch
default: "true"
outputs:
cache-key:
@@ -25,6 +31,9 @@ outputs:
cache-matched-key:
description: The compiled target cache key actually restored by actions/cache
value: ${{ steps.target.outputs.cache-matched-key }}
save-enabled:
description: Whether the caller permits publishing cache snapshots
value: ${{ inputs.save-if }}
legacy-fallback-invoked:
description: Whether the optional legacy cache fallback action was invoked
value: ${{ steps.cache-summary.outputs.legacy-fallback-invoked }}
@@ -42,7 +51,7 @@ runs:
CACHE_OS: ${{ runner.os }}
CACHE_ARCH: ${{ runner.arch }}
CACHE_WORKLOAD: ${{ inputs.key }}
CACHE_REVISION: ${{ github.sha }}
CACHE_REVISION: ${{ inputs.revision || github.sha }}
CACHE_MANIFESTS: ${{ hashFiles('Cargo.lock', '**/Cargo.toml', 'rust-toolchain.toml', '**/.cargo/config.toml', '.github/ci-profile.toml') }}${{ inputs.extra-key }}
run: |
python - <<'PY'
@@ -66,8 +75,8 @@ runs:
("CACHE_OS", "CACHE_ARCH", "CACHE_WORKLOAD"))
prefix += "-" + fingerprint + "-"
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output:
# Immutable caches need a new entry after a source change, including
# when the prior revision failed before it compiled every profile.
# 发布任务按源码刷新;原生 CI 可固定代次,只在依赖或工具链变化后新建快照。
# 缓存命中只复用编译输入,调用方仍须执行当前提交的全部检查。
output.write("key=" + prefix + os.environ["CACHE_MANIFESTS"] + "-" + os.environ["CACHE_REVISION"] + "\n")
output.write("manifest-key=" + prefix + os.environ["CACHE_MANIFESTS"] + "\n")
output.write("restore-key=" + prefix + "\n")
@@ -92,8 +101,11 @@ runs:
${{ steps.identity.outputs.cargo-home }}/registry/cache
${{ steps.identity.outputs.cargo-home }}/git/db
${{ steps.identity.outputs.cargo-home }}/git/checkouts
key: cargo-downloads-v2-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('Cargo.lock', '**/Cargo.toml') }}
restore-keys: cargo-downloads-v2-${{ runner.os }}-${{ runner.arch }}-
# 同一 OS 的两种架构共享源码,编译产物仍按架构隔离。
key: cargo-downloads-v3-${{ runner.os }}-${{ hashFiles('Cargo.lock', '**/Cargo.toml') }}
restore-keys: |
cargo-downloads-v3-${{ runner.os }}-
cargo-downloads-v2-${{ runner.os }}-${{ runner.arch }}-
- name: Restore compiled targets
id: target
uses: actions/cache/restore@v4
@@ -127,6 +139,7 @@ runs:
CACHE_WORKLOAD: ${{ inputs.key }}
RUNNER_OS_VALUE: ${{ runner.os }}
RUNNER_ARCH_VALUE: ${{ runner.arch }}
CACHE_SAVE_ENABLED: ${{ inputs.save-if }}
run: |
set -euo pipefail
@@ -148,6 +161,7 @@ runs:
echo "- Target cache exact hit: \`$target_cache_hit\`"
echo "- Target cache matched key: \`$target_cache_matched_key\`"
echo "- Save key: \`$save_key\`"
echo "- Cache publishing enabled: \`$CACHE_SAVE_ENABLED\`"
echo "- Legacy fallback configured: \`$legacy_configured\`"
echo "- Legacy fallback invoked: \`$legacy_invoked\`"
echo "- Legacy fallback outcome: \`$legacy_outcome\`"
@@ -159,14 +173,27 @@ runs:
echo "legacy-fallback-outcome=$legacy_outcome"
} >> "$GITHUB_OUTPUT"
- name: Populate complete downloads before sharing the cache
if: steps.downloads.outputs.cache-hit != 'true'
if: inputs.save-if == 'true' && steps.downloads.outputs.cache-hit != 'true'
shell: bash
env:
CACHE_OS: ${{ runner.os }}
run: |
host_target="$(rustc -vV | sed -n 's/^host: //p')"
test -n "$host_target"
cargo fetch --locked --target "$host_target"
# 缓存由先完成的架构写入,必须预取两者的源码,不能把半份下载标成精确命中。
case "$CACHE_OS" in
Windows)
cargo fetch --locked --target x86_64-pc-windows-msvc --target aarch64-pc-windows-msvc
;;
macOS)
cargo fetch --locked --target x86_64-apple-darwin --target aarch64-apple-darwin
;;
*)
host_target="$(rustc -vV | sed -n 's/^host: //p')"
test -n "$host_target"
cargo fetch --locked --target "$host_target"
;;
esac
- name: Save complete shared Cargo downloads
if: steps.downloads.outputs.cache-hit != 'true'
if: inputs.save-if == 'true' && steps.downloads.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
with:
path: |
+17 -11
View File
@@ -11,14 +11,15 @@ on:
push:
branches: [main]
# Include docs, catalogs, assets and workflow edits: every PR gets test results.
# ready_for_review starts the scarce-runner jobs that draft pushes skip.
# 草稿已跑完整矩阵,转为 Ready 而没有新提交时不重复申请七个原生 runner。
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
types: [opened, synchronize, reopened]
merge_group:
concurrency:
group: native-${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'merge_group' }}
# main 的过期验证也应释放 runner;手动诊断和发布调用不互相取消。
cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'merge_group' || (github.event_name == 'push' && github.ref == 'refs/heads/main') }}
permissions:
contents: read
@@ -75,9 +76,7 @@ jobs:
--output "$GITHUB_OUTPUT"
# The required lint job validates the matrix before heavy runners are
# requested. Draft PRs omit deferred platforms entirely; ready PRs and
# release callers require the full matrix. Deferred checks remain missing
# on drafts instead of being reported as successful tests.
# requested. Every PR, including a draft, requires the complete matrix.
native-tests:
needs: lint
if: ${{ inputs.windows_diagnostics_run == '' }}
@@ -131,6 +130,10 @@ jobs:
if ($LASTEXITCODE -ne 0 -or $compiler -notcontains 'host: aarch64-pc-windows-msvc') {
throw 'The Windows ARM job must compile and run ARM64 tests.'
}
- name: Install the pinned native test runner
uses: taiki-e/install-action@v2
with:
tool: cargo-nextest@0.9.146
- uses: ./.github/actions/rust-cache
id: rust-cache
with:
@@ -139,7 +142,9 @@ jobs:
target: |
target/ci
${{ runner.os != 'macOS' && 'target/release' || '' }}
legacy-key: ${{ runner.os == 'Windows' && 'windows' || runner.os == 'Linux' && 'linux' || 'macos' }}
# PR merge-ref 缓存不与其他 PR 共享;只由默认分支维护可复用的依赖快照。
revision: dependencies-v1
save-if: ${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
- name: Prepare pinned Windows console runtime
if: ${{ runner.os == 'Windows' }}
shell: pwsh
@@ -148,7 +153,7 @@ jobs:
./scripts/prepare-windows-runtime.ps1 -Destination assets/windows/conhost -Architecture $architecture
./scripts/tests/windows-runtime.tests.ps1 -Architecture $architecture
- name: Test complete workspace and native harness
run: python scripts/ci_native_tests.py
run: python scripts/ci_native_tests.py --runner nextest
- name: Check source architecture
if: ${{ !cancelled() && runner.os == 'Linux' }}
run: python scripts/check_architecture.py
@@ -159,7 +164,7 @@ jobs:
if: ${{ !cancelled() && runner.os != 'macOS' }}
run: cargo check --locked --workspace --release
- name: Save compiled workload
if: ${{ always() && steps.rust-cache.outcome == 'success' && steps.rust-cache.outputs.cache-hit != 'true' }}
if: ${{ success() && steps.rust-cache.outputs.save-enabled == 'true' && steps.rust-cache.outputs.cache-hit != 'true' }}
uses: actions/cache/save@v4
with:
path: |
@@ -214,11 +219,12 @@ jobs:
with:
key: native-release-check
target: target/release
legacy-key: macos
revision: dependencies-v1
save-if: ${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
- name: Check the release workspace
run: cargo check --locked --workspace --release --timings
- name: Save compiled workload
if: ${{ always() && steps.rust-cache.outcome == 'success' && steps.rust-cache.outputs.cache-hit != 'true' }}
if: ${{ success() && steps.rust-cache.outputs.save-enabled == 'true' && steps.rust-cache.outputs.cache-hit != 'true' }}
uses: actions/cache/save@v4
with:
path: target/release
+13 -8
View File
@@ -130,13 +130,18 @@ compile check into a claim that UI tests or a packaged application were run.
`Tests (<os>)` jobs and both `Release workspace (<os>)` jobs are required checks
on `main`, together with Code Owner approval; see the
[activation checklist](docs/project-constraints.md#server-side-activation).
Open pull requests as drafts while iterating: drafts run Linux, Windows x64 and
Apple Silicon tests plus the Apple Silicon release-profile compile check. The
required lint job validates the event and selects the matrix before requesting
platform runners. Drafts omit Intel Mac and Windows ARM64 jobs entirely, so their
required contexts may be missing until the PR is marked ready. Ready PRs run the
full matrix; every required check must succeed before merging. The ready event
still reruns core validation rather than reusing an earlier check conclusion.
Draft and ready pull requests both run all five native platforms and both macOS
release-profile compile checks. The required lint job validates the event and
selects the matrix before requesting platform runners. Every required check must
succeed before merging. Marking a draft ready without changing its commits does
not repeat the matrix; source updates and reopen events run it again. Native CI
uses pinned `cargo-nextest` for unit and
integration tests, followed by `cargo test --doc` with the same workspace features;
the production feature graph and release workspace are still checked separately.
PRs restore Cargo caches without uploading merge-ref snapshots. The default branch
publishes reusable snapshots per dependency/toolchain configuration; a cache hit
never skips current-commit tests. Downloads are shared between architectures of
the same OS, while compiled targets remain isolated by architecture and SDK.
Local hooks are convenient, but bypassable; they are not the enforcement boundary.
Submitting a workflow or `CODEOWNERS` file does not configure server-side rules.
@@ -150,7 +155,7 @@ feature work; there is no routine `--skip-architecture` option.
- 先读架构图、工程合同和决策记录;按职责拆分,不按行号切片。
- 一个 PR 只做一件事;改动超过 1500 行源码(不计文档、lockfile、资源)`pr-size` 会失败,请拆分。
- Draft PR 先运行必需的格式检查和矩阵规划,再创建 Linux / Windows x64 / Apple Silicon 测试及 Apple Silicon release 编译检查;不创建 Intel Mac / Windows ARM64 任务。Ready 后运行完整矩阵,十项必需检查全绿才能合并。
- Draft 和 Ready PR 都先运行必需的格式检查和矩阵规划,再执行五平台原生测试及两项 macOS release 编译检查;十项必需检查全绿才能合并。提速使用共享缓存与并行测试,不省略平台或 doctest。
- 2000 行是现有仓库的防灾上限,800 行只提示审查,不是“大厂标准”。
- 普通功能 PR 不得增加存量债务;有问题的规则可以修订,但要有反例、测试和维护者审批。
- 新增核心抽象、依赖方向、持久化或线程模型改变要先说明设计,不强迫每个小修复写 ADR。
@@ -1,5 +1,7 @@
# Plan the native matrix before requesting runners
Draft coverage policy superseded by [shared native PR caches](2026-09-25-shared-native-pr-cache.md).
## Status
Implemented for review. Preserves the existing ten required check names and
@@ -0,0 +1,122 @@
# Speed up native PR checks without dropping platform coverage
## Status
Implemented for review. The five native check names and two macOS release-check
names are unchanged. Hosted-runner measurements are pending the independent PR;
this change does not modify GitHub rulesets or claim a fixed completion time.
## Context
PR speed must improve without omitting native architectures, doctests, production
features or release-profile compilation. Draft PRs now have the same coverage as
ready PRs. Existing test assertions and failure propagation remain in force.
## Evidence
In [run 36119856851](https://github.com/Kuddev/pebrel/actions/runs/36119856851),
the Intel Mac job took 23m55s, including 12m59s of test compilation after both
cache layers missed. In
[run 36084876307](https://github.com/Kuddev/pebrel/actions/runs/36084876307), its
queue alone took 109m24s. Queue time and test execution are different bottlenecks.
Inspected native target snapshots were approximately 1–1.37 GB. Saving a new
snapshot for every PR commit consumes repository storage even though a sibling
PR cannot restore that merge-ref cache. GitHub documents this scope boundary and
the default cache limit in its
[dependency caching reference](https://docs.github.com/en/actions/reference/workflows-and-actions/dependency-caching).
Default-branch caches can be restored by PRs.
Nextest 0.9.146 publishes native Windows ARM64 and x64 binaries and a universal
macOS binary. Its CLI supports the current Cargo profile and timing options.
[Nextest](https://nexte.st/docs/running/) runs unit/integration tests separately
but does not replace Cargo's doctest execution.
The first hosted nextest run, [Linux job 108135574634](https://github.com/Kuddev/pebrel/actions/runs/36154433909/job/108135574634),
ran 2,245 tests and exposed six theme-studio fixture conflicts. Those tests already
share `lock_theme_studio` because they read and restore the same settings file.
An in-process mutex does not protect separate nextest processes. Nextest's
[test groups](https://nexte.st/docs/configuration/test-groups/) provide the
corresponding cross-process scheduling boundary.
## Decision
- The existing validated platform catalog selects all five native platforms and
both macOS release checks for draft and ready PRs. Keep the lint prerequisite,
unfiltered PR triggers, native Windows ARM compiler assertion and SDK checks.
Readiness alone no longer reruns the matrix: its commit already receives full
coverage on `opened`/`synchronize`, and `reopened` still revalidates the PR.
- Native CI installs pinned nextest, runs the complete workspace without test
filters or automatic retries, and explicitly runs `cargo test --doc` with the
same profile/features. Keep the independent production-feature check. The
script's default remains Cargo for existing local and release callers.
- Give the entire existing theme-studio fixture module one nextest group with
`max-threads = 1`, including readers and writers. This restores its previous
mutual exclusion without serializing other tests or changing assertions.
- Native jobs restore caches on PRs but publish only on the default branch.
Use a fixed `dependencies-v1` snapshot suffix together with the existing
compiler, OS, architecture, workload, SDK, flags and manifest identity. Source
changes still run Cargo and all tests; cache hits never substitute results.
- Save the immutable native snapshot only after successful checks. A failed
first attempt must not permanently publish an incomplete snapshot under the
fixed key. Dependency/toolchain changes create new keys; deliberate generation
changes can refresh an otherwise unchanged snapshot.
- Share Cargo downloads between architectures of the same OS. Before publishing,
fetch both supported Windows or macOS targets. Keep compiled targets separate.
Read-only PR consumers fetch only what their actual commands require. Restore
old per-architecture downloads during the cache transition.
- Cancel obsolete main validation runs as well as obsolete PR/merge-group runs.
Do not add more macOS runner jobs or change required status contexts.
## Rejected alternatives
- Omit draft, docs-only or platform-specific PR tests: violates the required
cross-platform coverage.
- Drop doctests or the production feature graph when adopting nextest: they
cover contracts not replaced by its unit/integration test execution.
- Remove macOS release checks or fold them back into the long native test job:
they remain required, and prior execution measurements motivated parallelism.
- Cache a new full target directory under every source SHA: duplicates large
snapshots while merge-ref scope prevents reuse by other PRs.
- Add sccache and incremental compilation together: sccache's Rust cache requires
incremental compilation to be disabled and does not cache executable linking.
This change leaves the existing tested compiler profile intact.
- Retry failing tests until green or copy earlier check conclusions: obscures
current-commit failures rather than making validation more efficient.
## Consequences
There are still seven native runner jobs for every PR. Drafts request more jobs
than the superseded policy, not fewer. Shared caches and parallel test execution
target avoidable work; hosted-runner capacity still determines queue latency.
The first successful default-branch run seeds each new snapshot. A dependency
change can still cause a cold build. Release/package callers retain SHA-based
target snapshots; this PR does not change their build or publication semantics.
## Validation
The existing planner, native-workflow, cache, stable-release, platform-cfg and
PR-size contract suites ran 58 tests locally: 57 passed, one POSIX-only summary
execution test was skipped on Windows. Tests cover full draft/ready matrices,
read-only PR cache behavior, cross-architecture download completeness, retained
doctests/features and failure propagation at each command.
Actionlint 1.7.12 passed for the native workflow. The architecture check passed
against the PR base without policy/budget changes. Nextest ran all 71
`nebula-settings` tests successfully on Windows; its discovered test names match
Cargo's inventory exactly. Rustfmt and the subset's doctest command also passed
(that subset has zero doctests). Full native/GPUI execution,
cache restore/save behavior and cold/warm timing comparisons are validated by
the hosted PR runs, not inferred from the local subset.
## Supersedes
The draft coverage reduction in
[native matrix planning](2026-09-23-native-matrix-planning.md). Its validated
event planning and required lint ownership remain unchanged.
## Revisit when
Measured cache hit rates, archive transfer costs, nextest process overhead or
runner queue times justify a different bounded cache or test scheduling policy.
+24 -7
View File
@@ -3,19 +3,32 @@
from __future__ import annotations
import argparse
import subprocess
import sys
from typing import Sequence
def native_commands() -> list[list[str]]:
def native_commands(runner: str = "cargo") -> list[list[str]]:
profile = ["--config", ".github/ci-profile.toml", "--profile", "ci"]
workspace = ["--locked", "--workspace", "--features", "nebula/gpui-test-support", "--timings"]
if runner == "cargo":
tests = [["cargo", "test", *profile, *workspace]]
elif runner == "nextest":
tests = [
[
"cargo", "nextest", "run", "--config", ".github/ci-profile.toml",
"--cargo-profile", "ci", *workspace, "--no-fail-fast", "--retries", "0",
],
# nextest 不执行 doctest;显式补跑,提速不改变 workspace 的覆盖范围。
["cargo", "test", *profile, *workspace, "--doc"],
]
else:
raise ValueError(f"unknown test runner: {runner}")
return [
[sys.executable, "-m", "unittest", "discover", "-s", "scripts/tests", "-v"],
[sys.executable, "-m", "unittest", "discover", "-s", "scripts/conformance/tests", "-v"],
[
"cargo", "test", "--locked", *profile, "--workspace",
"--features", "nebula/gpui-test-support", "--timings",
],
*tests,
# Link the test graph first: check can reuse compatible compiled
# dependencies, while metadata-only check output cannot link the tests.
# Keep the actual production feature graph independently checked.
@@ -26,8 +39,12 @@ def native_commands() -> list[list[str]]:
]
def main() -> int:
for command in native_commands():
def main(argv: Sequence[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
# 发布脚本和本地入口保留 cargo 默认值,仅原生 CI 显式选择已安装的 nextest。
parser.add_argument("--runner", choices=("cargo", "nextest"), default="cargo")
arguments = parser.parse_args(argv)
for command in native_commands(arguments.runner):
print("Running:", " ".join(command), flush=True)
subprocess.run(command, check=True)
return 0
+13 -16
View File
@@ -1,9 +1,8 @@
#!/usr/bin/env python3
"""Plan the native CI matrices before GitHub creates runner jobs.
The output file is the path normally supplied as ``GITHUB_OUTPUT``. Planning
is deliberately separate from workflow steps: a draft pull request therefore
does not materialize jobs for the scarce platforms.
The output file is the path normally supplied as ``GITHUB_OUTPUT``. Validate
the event before requesting runners; draft and ready PRs get the same coverage.
"""
from __future__ import annotations
@@ -19,17 +18,16 @@ from typing import Any, Sequence
@dataclass(frozen=True)
class Platform:
runner: str
draft: bool
release_check: bool = False
# One platform catalog owns both draft eligibility and release compilation.
# 所有 PR 共用平台清单,避免提速时把草稿的原生覆盖悄悄省略。
PLATFORMS = (
Platform("ubuntu-24.04", draft=True),
Platform("windows-2022", draft=True),
Platform("macos-26", draft=True, release_check=True),
Platform("windows-11-arm", draft=False),
Platform("macos-26-intel", draft=False, release_check=True),
Platform("ubuntu-24.04"),
Platform("windows-2022"),
Platform("macos-26", release_check=True),
Platform("windows-11-arm"),
Platform("macos-26-intel", release_check=True),
)
FULL_EVENTS = frozenset(
@@ -41,11 +39,10 @@ class PlanError(ValueError):
"""An event cannot be converted into a safe CI plan."""
def _matrices(draft: bool) -> tuple[list[dict[str, str]], list[dict[str, str]]]:
selected = [platform for platform in PLATFORMS if not draft or platform.draft]
def _matrices() -> tuple[list[dict[str, str]], list[dict[str, str]]]:
return (
[{"os": platform.runner} for platform in selected],
[{"os": platform.runner} for platform in selected if platform.release_check],
[{"os": platform.runner} for platform in PLATFORMS],
[{"os": platform.runner} for platform in PLATFORMS if platform.release_check],
)
@@ -79,10 +76,10 @@ def plan_matrices(event_name: str, payload: dict[str, Any]) -> tuple[list[dict[s
draft = pull_request.get("draft")
if not isinstance(draft, bool):
raise PlanError("pull_request.draft must be a boolean")
return _matrices(draft)
return _matrices()
if event_name in FULL_EVENTS:
return _matrices(draft=False)
return _matrices()
supported = sorted(FULL_EVENTS | {"pull_request"})
raise PlanError(f"unsupported event name {event_name!r}; expected one of {', '.join(supported)}")
+33 -2
View File
@@ -108,6 +108,7 @@ class CacheIdentityTests(unittest.TestCase):
"Target cache exact hit",
"Target cache matched key",
"Save key",
"Cache publishing enabled",
"Legacy fallback invoked",
"Legacy fallback outcome",
):
@@ -149,21 +150,51 @@ class CacheIdentityTests(unittest.TestCase):
"GITHUB_OUTPUT": str(root / "outputs"),
"TARGET_CACHE_HIT": hit, "TARGET_CACHE_MATCHED_KEY": matched,
"SAVE_KEY": "new-save-key", "LEGACY_CONFIGURED": configured,
"CACHE_SAVE_ENABLED": "false",
"LEGACY_OUTCOME": outcome, "CACHE_WORKLOAD": workload,
"RUNNER_OS_VALUE": "Linux", "RUNNER_ARCH_VALUE": "X64",
}
result = subprocess.run([shutil.which("bash"), "-c", script], env=environment,
text=True, capture_output=True, check=False)
self.assertEqual(result.returncode, 0, result.stderr)
summary = (root / "summary").read_text()
outputs = (root / "outputs").read_text()
summary = (root / "summary").read_text(encoding="utf-8")
outputs = (root / "outputs").read_text(encoding="utf-8")
self.assertIn(f"Target cache exact hit: `{hit or 'false'}`", summary)
self.assertIn(f"Target cache matched key: `{matched or '<none>'}`", summary)
self.assertIn("Cache publishing enabled: `false`", summary)
self.assertIn(f"legacy-fallback-invoked={invoked}\n", outputs)
self.assertIn(f"legacy-fallback-outcome={outcome}\n", outputs)
self.assertIn(workload, summary)
self.assertFalse(marker.exists(), "summary values must never execute as shell code")
def test_dependency_snapshot_revision_preserves_compatibility_boundaries(self):
source = self.action_text()
self.assertIn("CACHE_REVISION: ${{ inputs.revision || github.sha }}", source)
snapshot = self.identity(CACHE_REVISION="dependencies-v1")
changed = self.identity(CACHE_REVISION="dependencies-v1", CACHE_MANIFESTS="new-dependencies")
self.assertNotEqual(snapshot["key"], changed["key"])
self.assertEqual(snapshot["restore-key"], changed["restore-key"])
def test_source_downloads_share_architectures_but_targets_do_not(self):
source = self.action_text()
self.assertIn("key: cargo-downloads-v3-${{ runner.os }}-${{ hashFiles('Cargo.lock', '**/Cargo.toml') }}", source)
self.assertIn("cargo-downloads-v2-${{ runner.os }}-${{ runner.arch }}-", source)
fetch = source.split("- name: Populate complete downloads before sharing the cache", 1)[1]
for targets in (
"--target x86_64-pc-windows-msvc --target aarch64-pc-windows-msvc",
"--target x86_64-apple-darwin --target aarch64-apple-darwin",
):
self.assertIn(f"cargo fetch --locked {targets}", fetch)
self.assertNotEqual(self.identity(CACHE_ARCH="X64")["restore-key"],
self.identity(CACHE_ARCH="ARM64")["restore-key"])
def test_read_only_consumers_neither_prefetch_other_architectures_nor_save(self):
source = self.action_text()
self.assertIn("value: ${{ inputs.save-if }}", source)
for step in ("Populate complete downloads before sharing the cache", "Save complete shared Cargo downloads"):
body = source.split(f"- name: {step}", 1)[1].split("\n - name:", 1)[0]
self.assertIn("if: inputs.save-if == 'true' && steps.downloads.outputs.cache-hit != 'true'", body)
@unittest.skipUnless(os.name == "nt", "Windows cache migration")
def test_windows_migration_preserves_compiler_and_workload_boundaries(self):
before = self.identity()
+91 -25
View File
@@ -13,7 +13,7 @@ from scripts.ci_native_tests import main, native_commands
class NativeSuiteTests(unittest.TestCase):
def test_required_lint_plans_runners_before_native_jobs(self):
root = Path(__file__).resolve().parents[2]
workflow = (root / ".github/workflows/linux-lua.yml").read_text()
workflow = (root / ".github/workflows/linux-lua.yml").read_text(encoding="utf-8")
lint = workflow.split("\n lint:\n", 1)[1].split("\n native-tests:\n", 1)[0]
self.assertIn("name: lint", lint)
self.assertIn("Test CI contracts before matrix planning", lint)
@@ -61,7 +61,7 @@ class NativeSuiteTests(unittest.TestCase):
def test_packages_run_after_merge_or_manual_dispatch_not_for_prs(self):
root = Path(__file__).resolve().parents[2]
workflow = (root / ".github/workflows/preview-packages.yml").read_text()
workflow = (root / ".github/workflows/preview-packages.yml").read_text(encoding="utf-8")
events = workflow.split("\non:\n", 1)[1].split("\nconcurrency:", 1)[0]
triggers = set(re.findall(r"^ ([a-z_]+):", events, re.M))
self.assertEqual(triggers, {"push", "workflow_dispatch"})
@@ -71,7 +71,7 @@ class NativeSuiteTests(unittest.TestCase):
self.assertIn("paths:", push.group(1))
# Packaging still needs explicit dispatch to create a public release.
self.assertIn("github.event_name == 'workflow_dispatch' && inputs.publish == true", workflow)
stable = (root / ".github/workflows/release.yml").read_text()
stable = (root / ".github/workflows/release.yml").read_text(encoding="utf-8")
stable_events = stable.split("\non:\n", 1)[1].split("\nconcurrency:", 1)[0]
self.assertEqual(set(re.findall(r"^ ([a-z_]+):", stable_events, re.M)),
{"push", "workflow_dispatch"})
@@ -79,7 +79,7 @@ class NativeSuiteTests(unittest.TestCase):
def test_every_pr_and_merge_group_runs_without_path_exclusions(self):
root = Path(__file__).resolve().parents[2]
workflow = (root / ".github/workflows/linux-lua.yml").read_text()
workflow = (root / ".github/workflows/linux-lua.yml").read_text(encoding="utf-8")
events = workflow.split("\non:\n", 1)[1].split("\nconcurrency:", 1)[0]
for event in ("pull_request", "merge_group"):
declaration = re.search(rf"^ {event}:(.*?)(?=^ [a-z_]+:|\Z)", events, re.M | re.S)
@@ -87,9 +87,9 @@ class NativeSuiteTests(unittest.TestCase):
self.assertNotIn("paths", declaration.group(1))
self.assertNotIn("branches", declaration.group(1))
if event == "pull_request":
# ready_for_review starts the scarce-runner jobs that draft
# pushes skip; the default activity types must stay listed.
self.assertIn("types: [opened, synchronize, reopened, ready_for_review]", declaration.group(1))
# Drafts already run the full matrix; readiness alone changes no source.
self.assertIn("types: [opened, synchronize, reopened]", declaration.group(1))
self.assertNotIn("ready_for_review", declaration.group(1))
else:
self.assertNotIn("types", declaration.group(1))
self.assertIn("branches: [main]", events)
@@ -100,14 +100,14 @@ class NativeSuiteTests(unittest.TestCase):
def test_arm_job_requires_native_execution_and_matching_console_runtime(self):
root = Path(__file__).resolve().parents[2]
workflow = (root / ".github/workflows/linux-lua.yml").read_text()
workflow = (root / ".github/workflows/linux-lua.yml").read_text(encoding="utf-8")
self.assertIn("windows-11-arm", workflow)
self.assertIn("host: aarch64-pc-windows-msvc", workflow)
self.assertIn("OSArchitecture -ne 'Arm64'", workflow)
self.assertIn("-Architecture $architecture", workflow)
self.assertLess(workflow.index("Prepare pinned Windows console runtime"),
workflow.index("Test complete workspace"))
preview = (root / ".github/workflows/preview-packages.yml").read_text()
preview = (root / ".github/workflows/preview-packages.yml").read_text(encoding="utf-8")
windows = preview.split("\n windows:\n", 1)[1].split("\n aggregate:", 1)[0]
self.assertNotIn("prepare-windows-runtime.ps1", preview.split("\njobs:", 1)[1].split("\n windows:", 1)[0])
self.assertLess(windows.index("prepare-windows-runtime.ps1"),
@@ -128,16 +128,75 @@ class NativeSuiteTests(unittest.TestCase):
self.assertNotIn("--", command)
def test_actual_product_feature_graph_is_also_checked(self):
checks = [command for command in native_commands() if command[:2] == ["cargo", "check"]]
self.assertEqual(len(checks), 1)
command = checks[0]
self.assertEqual(command[command.index("--features") + 1], "gpui-shell")
self.assertEqual(command[command.index("--bin") + 1], "pebrel")
for runner in ("cargo", "nextest"):
with self.subTest(runner=runner):
checks = [command for command in native_commands(runner) if command[:2] == ["cargo", "check"]]
self.assertEqual(len(checks), 1)
command = checks[0]
self.assertEqual(command[command.index("--features") + 1], "gpui-shell")
self.assertEqual(command[command.index("--bin") + 1], "pebrel")
def test_nextest_runs_all_native_targets_and_preserves_doctests(self):
commands = native_commands("nextest")
tests = [command for command in commands if command[:3] == ["cargo", "nextest", "run"]]
docs = [command for command in commands if command[:2] == ["cargo", "test"]]
self.assertEqual(len(tests), 1)
self.assertEqual(len(docs), 1)
self.assertIn("--doc", docs[0])
self.assertNotIn("--doc", tests[0])
for command, profile_flag in ((tests[0], "--cargo-profile"), (docs[0], "--profile")):
self.assertIn("--locked", command)
self.assertIn("--workspace", command)
self.assertIn("--timings", command)
self.assertEqual(command[command.index("--features") + 1], "nebula/gpui-test-support")
self.assertEqual(command[command.index(profile_flag) + 1], "ci")
self.assertEqual(command[command.index("--config") + 1], ".github/ci-profile.toml")
for excluded in ("--exclude", "--lib", "--skip", "--filterset", "--partition", "--no-run"):
self.assertNotIn(excluded, command)
self.assertIn("--no-fail-fast", tests[0])
self.assertEqual(tests[0][tests[0].index("--retries") + 1], "0")
self.assertLess(commands.index(tests[0]), commands.index(docs[0]))
def test_native_workflow_installs_nextest_without_changing_release_callers(self):
root = Path(__file__).resolve().parents[2]
workflow = (root / ".github/workflows/linux-lua.yml").read_text(encoding="utf-8")
native = workflow.split("\n native-tests:\n", 1)[1].split("\n macos-release-check:\n", 1)[0]
install = native.split("- name: Install the pinned native test runner", 1)[1].split(" - uses:", 1)[0]
self.assertIn("uses: taiki-e/install-action@v2", install)
self.assertIn("tool: cargo-nextest@0.9.146", install)
self.assertNotIn("if:", install)
self.assertIn("run: python scripts/ci_native_tests.py --runner nextest", native)
release = (root / ".github/workflows/release.yml").read_text(encoding="utf-8")
self.assertIn("run: python scripts/ci_native_tests.py\n", release)
def test_nextest_preserves_only_the_existing_shared_theme_fixture_mutex(self):
root = Path(__file__).resolve().parents[2]
config = tomllib.loads((root / ".config/nextest.toml").read_text(encoding="utf-8"))
self.assertEqual(config["test-groups"], {"theme-studio": {"max-threads": 1}})
self.assertEqual(config["profile"]["default"], {
"overrides": [{
"filter": "test(gpui_shell::settings_pane::theme_studio_tests::)",
"test-group": "theme-studio",
}],
})
def test_native_caches_are_default_branch_snapshots_not_per_pr_uploads(self):
root = Path(__file__).resolve().parents[2]
workflow = (root / ".github/workflows/linux-lua.yml").read_text(encoding="utf-8")
for job in ("native-tests", "macos-release-check"):
body = workflow.split(f"\n {job}:\n", 1)[1]
body = re.split(r"\n [a-z][a-z-]*:\n", body, maxsplit=1)[0]
self.assertIn("revision: dependencies-v1", body)
self.assertIn("save-if: ${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}", body)
save = body.split("- name: Save compiled workload", 1)[1].split(" - name:", 1)[0]
self.assertIn("success() && steps.rust-cache.outputs.save-enabled == 'true'", save)
self.assertNotIn("always()", save)
self.assertIn("steps.rust-cache.outputs.cache-hit != 'true'", save)
def test_fast_test_profile_preserves_runtime_checks_and_resets_named_overrides(self):
root = Path(__file__).resolve().parents[2]
config = tomllib.loads((root / ".github/ci-profile.toml").read_text())
workspace = tomllib.loads((root / "Cargo.toml").read_text())
config = tomllib.loads((root / ".github/ci-profile.toml").read_text(encoding="utf-8"))
workspace = tomllib.loads((root / "Cargo.toml").read_text(encoding="utf-8"))
profile = config["profile"]["ci"]
self.assertEqual(profile["inherits"], "dev")
self.assertTrue(profile["debug-assertions"])
@@ -155,17 +214,24 @@ class NativeSuiteTests(unittest.TestCase):
self.assertTrue(all("discover" in command for command in suites))
def test_success_runs_every_command_and_checks_exit_codes(self):
with patch("scripts.ci_native_tests.subprocess.run") as run:
self.assertEqual(main(), 0)
self.assertEqual(run.call_count, len(native_commands()))
self.assertTrue(all(call.kwargs["check"] for call in run.call_args_list))
for runner in ("cargo", "nextest"):
with self.subTest(runner=runner), patch("scripts.ci_native_tests.subprocess.run") as run:
self.assertEqual(main(["--runner", runner]), 0)
self.assertEqual([call.args[0] for call in run.call_args_list], native_commands(runner))
self.assertTrue(all(call.kwargs["check"] for call in run.call_args_list))
def test_failure_stops_the_suite_and_is_not_reported_as_success(self):
with patch("scripts.ci_native_tests.subprocess.run") as run:
run.side_effect = subprocess.CalledProcessError(7, "test")
with self.assertRaises(subprocess.CalledProcessError):
main()
self.assertEqual(run.call_count, 1)
for runner in ("cargo", "nextest"):
for failed in range(len(native_commands(runner))):
with self.subTest(runner=runner, failed=failed), patch("scripts.ci_native_tests.subprocess.run") as run:
run.side_effect = [None] * failed + [subprocess.CalledProcessError(7, "test")]
with self.assertRaises(subprocess.CalledProcessError):
main(["--runner", runner])
self.assertEqual(run.call_count, failed + 1)
def test_unknown_runner_is_rejected_before_running_commands(self):
with self.assertRaises(ValueError):
native_commands("typo")
if __name__ == "__main__":
+8 -5
View File
@@ -36,7 +36,7 @@ class CiPlanCliTests(unittest.TestCase):
lines = path.read_text(encoding="utf-8").splitlines()
return {name: json.loads(value) for name, value in (line.split("=", 1) for line in lines)}
def test_draft_pr_does_not_materialize_scarce_runner_rows(self) -> None:
def test_draft_pr_has_the_same_full_native_coverage_as_ready_pr(self) -> None:
with tempfile.TemporaryDirectory() as directory:
output = Path(directory) / "github-output"
result = self.run_cli("pull_request", {"pull_request": {"draft": True}}, output)
@@ -44,12 +44,15 @@ class CiPlanCliTests(unittest.TestCase):
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(
output.read_text(encoding="utf-8"),
'native_matrix=[{"os":"ubuntu-24.04"},{"os":"windows-2022"},{"os":"macos-26"}]\n'
'release_matrix=[{"os":"macos-26"}]\n',
'native_matrix=[{"os":"ubuntu-24.04"},{"os":"windows-2022"},{"os":"macos-26"},'
'{"os":"windows-11-arm"},{"os":"macos-26-intel"}]\n'
'release_matrix=[{"os":"macos-26"},{"os":"macos-26-intel"}]\n',
)
plan = self.outputs(output)
self.assertNotIn({"os": "windows-11-arm"}, plan["native_matrix"])
self.assertNotIn({"os": "macos-26-intel"}, plan["native_matrix"])
ready_output = Path(directory) / "ready-output"
ready = self.run_cli("pull_request", {"pull_request": {"draft": False}}, ready_output)
self.assertEqual(ready.returncode, 0, ready.stderr)
self.assertEqual(plan, self.outputs(ready_output))
def test_ready_pr_has_all_native_and_release_checks(self) -> None:
with tempfile.TemporaryDirectory() as directory: