Merge branch 'feat/cifix2-stabilize-mtime-probe-and-annotat'
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / unsafe 落点守门(集合必须恰好等于期望) (push) Canceled after 0s
Guards / design 零第三方依赖守门 (push) Canceled after 0s
Guards / presenter 零 UI 守门 (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Dev Guards / 架构与合规守门(同 main 口径) (push) Canceled after 0s
GPU evidence / GPU 证据 — 真实产物判据 (push) Canceled after 0s
GPU evidence / GPU 证据 — 探测可用产物与硬件适配器 (push) Canceled after 0s
GPU evidence / GPU 判据自测(不需 GPU;跳过 ≠ 已验证) (push) Canceled after 0s
Dev Guards / GPU 证据门禁(同 main 口径;无 GPU 只跑自测) (push) Canceled after 0s
Dev Guards / fmt (rustfmt --check) (push) Canceled after 0s
Dev Guards / clippy (ubuntu) (push) Canceled after 0s
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / unsafe 落点守门(集合必须恰好等于期望) (push) Canceled after 0s
Guards / design 零第三方依赖守门 (push) Canceled after 0s
Guards / presenter 零 UI 守门 (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Dev Guards / 架构与合规守门(同 main 口径) (push) Canceled after 0s
GPU evidence / GPU 证据 — 真实产物判据 (push) Canceled after 0s
GPU evidence / GPU 证据 — 探测可用产物与硬件适配器 (push) Canceled after 0s
GPU evidence / GPU 判据自测(不需 GPU;跳过 ≠ 已验证) (push) Canceled after 0s
Dev Guards / GPU 证据门禁(同 main 口径;无 GPU 只跑自测) (push) Canceled after 0s
Dev Guards / fmt (rustfmt --check) (push) Canceled after 0s
Dev Guards / clippy (ubuntu) (push) Canceled after 0s
This commit is contained in:
commit
a9f691e3ff
2 files changed
+69
-36
No files matched your search
+30
-29
@@ -109,8 +109,7 @@ jobs:
|
|||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
key: ${{ matrix.os }}
|
key: ${{ matrix.os }}
|
||||||
# `--workspace` 现在是 8 个 crate,不含 `unlucky-app`(M118 的 workspace exclude),
|
# `--workspace` 现在是 8 个 crate,不含 `unlucky-app`(M118 的 exclude)⇒ 不拉起 slint / usvg / i-slint-*。
|
||||||
# 所以这条命令不会拉起 slint / usvg / i-slint-*。
|
|
||||||
- run: cargo clippy --workspace --all-targets -- -D warnings
|
- run: cargo clippy --workspace --all-targets -- -D warnings
|
||||||
|
|
||||||
test:
|
test:
|
||||||
@@ -187,16 +186,15 @@ jobs:
|
|||||||
# 只会打一条 `excluded package(s) … not found in workspace` 的警告然后空转(实测 exit 0,不报错)。
|
# 只会打一条 `excluded package(s) … not found in workspace` 的警告然后空转(实测 exit 0,不报错)。
|
||||||
#
|
#
|
||||||
# ---- 取证(M147):下面是同一件事的两个平台版本,不是两条不同的测试 ----
|
# ---- 取证(M147):下面是同一件事的两个平台版本,不是两条不同的测试 ----
|
||||||
# M144 的第三条红 `test (windows)` 只留下 `Process completed with exit code 1`,而 cargo 自己的失败码
|
# M144 的 `test (windows)` 只留 `Process completed with exit code 1`(cargo 自己的失败码是 **101**)
|
||||||
# 是 **101** ⇒ 那个 1 的来源不明(包装层洗码 / 进程被杀,两说都未排除);job log 要登录才能读
|
# ⇒ 那个 1 来源不明;job log 要登录才能读(匿名 403),但**注解匿名可读**
|
||||||
# (匿名 403),但**注解匿名可读**(`GET /check-runs/<id>/annotations` → 200,M144 实证)。三件事
|
# (`GET /check-runs/<id>/annotations` → 200,M144 实证)。三件事各对准一种「看不见」:
|
||||||
# 各对准一种「看不见」:
|
# 1) `--no-fail-fast`:cargo 默认首个失败即停(M144 实测 Linux 只跑 7 个 test binary 就退出),
|
||||||
# 1) `--no-fail-fast`:cargo 默认在第一个失败的 target 就停(M144 实测 Linux 只跑 7 个 test
|
# 后面还有没有第二条失败看不到;
|
||||||
# binary 就退出),后面还有没有第二条失败完全看不到;
|
# 2) `error` / `failures:` / panicked 行升成 `::error::`:拿不到 job log 时注解是匿名侧唯一正文
|
||||||
# 2) `error` / `failures:` / panicked 行升成 `::error::` 注解:拿不到 job log 时,注解是匿名侧
|
# (M275 把同一手法补到末尾的 `update-e2e` 步骤);
|
||||||
# 唯一能取到的失败正文;
|
# 3) 显式打印 cargo 退出码:把「包装层把码洗成 1」「cargo 真失败(101)」「进程被杀只给码、
|
||||||
# 3) 显式打印 cargo 的退出码:把「包装层把码洗成 1」「cargo 真失败(101)」「进程被杀只给码、零诊断」
|
# 零诊断」分开——这正是 M144 没定的那一半。
|
||||||
# 分开——这正是 M144 没定的那一半。
|
|
||||||
- name: cargo test --workspace --all-targets(ubuntu,失败升注解)
|
- name: cargo test --workspace --all-targets(ubuntu,失败升注解)
|
||||||
if: runner.os == 'Linux'
|
if: runner.os == 'Linux'
|
||||||
run: |
|
run: |
|
||||||
@@ -364,20 +362,14 @@ jobs:
|
|||||||
path: ${{ runner.temp }}/smoke/
|
path: ${{ runner.temp }}/smoke/
|
||||||
if-no-files-found: warn
|
if-no-files-found: warn
|
||||||
|
|
||||||
# M169:更新链路的**端到端验收**(发现新版本 → 下载 → 长度/SHA256 双检 → 就位;篡改摘要 /
|
# M169:更新链路的**端到端验收**(发现新版本 → 下载 → 长度/SHA256 双检 → 就位;篡改摘要 / 篡改包体 /
|
||||||
# 篡改包体 / 截断必须拒绝且不动现有安装;重复 apply 的幂等终态)。
|
# 截断必须拒绝且不动现有安装;重复 apply 的幂等终态)。只在 Windows:真实产物是裸 `.exe` +
|
||||||
#
|
# `portable-exe`,Linux/macOS 是 `.tar.gz` + `portable-zip`(`ensure_self_replaceable` 直接拒),
|
||||||
# 独立 job、只在 Windows:夹具的「真形状」只在 Windows 成立——真实产物是裸 `.exe` +
|
# 在 ubuntu 上跑绿只证明「世上不存在的 linux portable-exe 能装上」(r1 复审 P2-4)。
|
||||||
# `portable-exe`,而 Linux/macOS 是 `.tar.gz` + `portable-zip`(会被 `ensure_self_replaceable`
|
# 步骤名 `update-e2e` 与 run 里的 `--test update_e2e_test` 被 `packaging/check_test_source_off.py` 的
|
||||||
# 直接拒)。在 ubuntu 上跑绿只能证明「一个世上不存在的 linux portable-exe 能装上」,是假绿
|
# `ALLOWED_STEP` 钉死(`update-test-source` 只许在这一项里出现)——改这里的 name / run 之前先读那段注释。
|
||||||
# (r1 复审 P2-4)。
|
# 慢是已知的(release + fat LTO,本机实测 7 分钟;不发 debug 档:`debug_assertions` / 优化 / panic 行为
|
||||||
#
|
# 都不同,用 debug 跑这条 E2E 就失去它存在的理由)。能跑起来比跑得快重要。
|
||||||
# 本 job 的步骤名 `update-e2e` 是**唯一**被允许出现 `update-test-source` 的地方,判据在
|
|
||||||
# `packaging/check_test_source_off.py` 的 `ALLOWED_STEP`:文件名 / 步骤名 / `--test
|
|
||||||
# update_e2e_test` 三者任一不符即红——改这里的 name 或 run 之前先读那段注释。
|
|
||||||
#
|
|
||||||
# 慢是已知的(release + fat LTO,本机实测 7 分钟;不发 debug 档:`debug_assertions`、优化与
|
|
||||||
# panic 行为都不一样,用 debug 跑这条 E2E 就失去了它存在的理由)。tower 明确:能跑起来比跑得快重要。
|
|
||||||
update-e2e:
|
update-e2e:
|
||||||
name: update-e2e (windows x64)
|
name: update-e2e (windows x64)
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
@@ -391,7 +383,16 @@ jobs:
|
|||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
key: windows-latest
|
key: windows-latest
|
||||||
|
# M275:裸 `cargo test` 失败只留一句 `exit code 1`、check-run 上零注解(匿名侧等于信息为零),所以照
|
||||||
|
# test 作业同一手法升注解:`$LASTEXITCODE` 是 cargo 的真码(Tee-Object 是 cmdlet,不改它)、`%` → `%25`。
|
||||||
- name: update-e2e
|
- name: update-e2e
|
||||||
run: >
|
shell: pwsh
|
||||||
cargo test --release -p unlucky-cli --features update-test-source
|
run: |
|
||||||
--test update_e2e_test --test update_e2e_tamper_test
|
$log = Join-Path $env:RUNNER_TEMP 'update-e2e.log'
|
||||||
|
cargo test --release -p unlucky-cli --features update-test-source --test update_e2e_test --test update_e2e_tamper_test 2>&1 | Tee-Object -FilePath $log
|
||||||
|
$code = $LASTEXITCODE; Write-Host "cargo exit=$code"
|
||||||
|
if ($code -ne 0) {
|
||||||
|
Select-String -Path $log -Pattern '(error(\[E\d+\])?:|^failures:|test result: FAILED|thread .* panicked at|^\s+--> )' | Select-Object -First 40 |
|
||||||
|
ForEach-Object { Write-Host "::error::$($_.Line.Replace('%', '%25'))" }
|
||||||
|
}
|
||||||
|
exit $code
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
//! 也抓不住「目录本来就在、但被重写/清空」的情形。
|
//! 也抓不住「目录本来就在、但被重写/清空」的情形。
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use unlucky_core::launch::natives::{self, NativeJar, NativesPlan};
|
use unlucky_core::launch::natives::{self, NativeJar, NativesPlan};
|
||||||
|
|
||||||
@@ -19,8 +20,9 @@ use crate::prelude::*;
|
|||||||
/// 瞬时写入只留父目录 mtime 一条线索,而 NTFS 会把该更新合并掉 —— 本仓实测:全量并行跑
|
/// 瞬时写入只留父目录 mtime 一条线索,而 NTFS 会把该更新合并掉 —— 本仓实测:全量并行跑
|
||||||
/// 测试时这条偶发看不见(单跑又看得见),所以瞬时建删只剩「`natives` 目录不存在」兜底。
|
/// 测试时这条偶发看不见(单跑又看得见),所以瞬时建删只剩「`natives` 目录不存在」兜底。
|
||||||
///
|
///
|
||||||
/// 时间戳口径:NTFS / ext4 的 mtime 精度是 100ns 级,快照取纳秒整数比较,同一秒内的
|
/// 时间戳口径:**精度 ≠ 可靠性**。NTFS / ext4 的 mtime 字段精度是 100ns 级,但「两个操作能不能
|
||||||
/// 两次操作(写文件 → 再写一次)分辨得出来,不需要 `sleep` 把操作推到不同秒。
|
/// 分辨」取决于时间戳**何时被采样、以多大粒度更新**(同内容立即重写在 NTFS 上可能落在同一系统
|
||||||
|
/// 时钟 tick 内、句柄关闭时才按粗粒度落盘),所以快照只在「有人显式改过时间戳」时才是判据。
|
||||||
/// 判据自检见 `tree_snapshot_sees_new_nodes_and_mtime_changes`(防假绿)。
|
/// 判据自检见 `tree_snapshot_sees_new_nodes_and_mtime_changes`(防假绿)。
|
||||||
fn tree_snapshot(root: &Path) -> Vec<String> {
|
fn tree_snapshot(root: &Path) -> Vec<String> {
|
||||||
fn line(meta: &std::fs::Metadata, relative: &str) -> String {
|
fn line(meta: &std::fs::Metadata, relative: &str) -> String {
|
||||||
@@ -211,14 +213,22 @@ fn dry_run_after_real_extract_reports_ready_without_writing() {
|
|||||||
///
|
///
|
||||||
/// 这里**不**用「建了立刻删」来探父目录 mtime:NTFS 会把瞬时建删对父目录 mtime 的影响
|
/// 这里**不**用「建了立刻删」来探父目录 mtime:NTFS 会把瞬时建删对父目录 mtime 的影响
|
||||||
/// 合并掉(本仓实测:全量并行跑时偶发看不见、单跑看得见),拿它当门禁就是引入 flaky。
|
/// 合并掉(本仓实测:全量并行跑时偶发看不见、单跑看得见),拿它当门禁就是引入 flaky。
|
||||||
/// 改成两种确定性变化:新节点(结构维度)+ 同内容重写(时间维度,只有文件 mtime 变,
|
///
|
||||||
/// 不依赖父目录时间戳的合并行为)。
|
/// 时间维度也**不**靠「同内容立刻重写」:NTFS 上 mtime 不保证「每次写都变」——同一个系统
|
||||||
|
/// 时钟 tick 内的两次写、或时间戳只在句柄关闭时按粗粒度落盘,都会让重写后的 mtime 与写之前
|
||||||
|
/// **逐字节相同**。M275 的实际失败正是这一条:main 上 `test (windows-latest)` 偶发红在这行
|
||||||
|
/// `assert_ne!`(`crates\core\tests\launch\natives_dry_run_cases.rs:241`),本机永远复现不出来。
|
||||||
|
/// `sleep` + 重试轮询只是把概率压低(把「不可靠观察」换成「不可靠观察 + 更长的墙钟」),
|
||||||
|
/// 不是消除。确定性判据是**显式时间戳**:`File::set_modified` 把 mtime 直接写成
|
||||||
|
/// 「基线 + 5s」——值由我们指定,与时钟 tick / 落盘粒度无关;快照若看不见它,说明快照真的
|
||||||
|
/// 丢了时间维度,而不是运气不好。
|
||||||
#[test]
|
#[test]
|
||||||
fn tree_snapshot_sees_new_nodes_and_mtime_changes() {
|
fn tree_snapshot_sees_new_nodes_and_mtime_changes() {
|
||||||
let root = temp_dir("natives-snapshot-probe");
|
let root = temp_dir("natives-snapshot-probe");
|
||||||
let version_dir = root.join("versions/1.16.5");
|
let version_dir = root.join("versions/1.16.5");
|
||||||
std::fs::create_dir_all(&version_dir).expect("建版本目录");
|
std::fs::create_dir_all(&version_dir).expect("建版本目录");
|
||||||
std::fs::write(version_dir.join("1.16.5.json"), b"{}").expect("放清单占位");
|
let manifest = version_dir.join("1.16.5.json");
|
||||||
|
std::fs::write(&manifest, b"{}").expect("放清单占位");
|
||||||
let baseline = tree_snapshot(&version_dir);
|
let baseline = tree_snapshot(&version_dir);
|
||||||
|
|
||||||
// 1) 结构维度:多一个目录节点(正是 `create_dir_all(natives)` 留下的痕迹)。
|
// 1) 结构维度:多一个目录节点(正是 `create_dir_all(natives)` 留下的痕迹)。
|
||||||
@@ -230,8 +240,20 @@ fn tree_snapshot_sees_new_nodes_and_mtime_changes() {
|
|||||||
);
|
);
|
||||||
assert_ne!(baseline, with_dir);
|
assert_ne!(baseline, with_dir);
|
||||||
|
|
||||||
// 2) 时间维度:同一份内容重写一遍,节点集合不变、文件 mtime 必须变。
|
// 2) 时间维度:显式把清单文件的 mtime 推到基线之后(节点集合不变 ⇒ 差异只可能来自时间戳)。
|
||||||
std::fs::write(version_dir.join("1.16.5.json"), b"{}").expect("重写清单占位");
|
// 不改内容长度(那动的是 size,不是 mtime)、不 `sleep`、不重试。
|
||||||
|
let before = std::fs::metadata(&manifest)
|
||||||
|
.expect("清单元数据")
|
||||||
|
.modified()
|
||||||
|
.expect("基线 mtime");
|
||||||
|
let pushed = before + Duration::from_secs(5);
|
||||||
|
let file = std::fs::File::options()
|
||||||
|
.write(true)
|
||||||
|
.open(&manifest)
|
||||||
|
.expect("打开清单占位");
|
||||||
|
file.set_modified(pushed).expect("把 mtime 推到基线之后");
|
||||||
|
drop(file);
|
||||||
|
|
||||||
let rewritten = tree_snapshot(&version_dir);
|
let rewritten = tree_snapshot(&version_dir);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
with_dir.len(),
|
with_dir.len(),
|
||||||
@@ -242,4 +264,14 @@ fn tree_snapshot_sees_new_nodes_and_mtime_changes() {
|
|||||||
with_dir, rewritten,
|
with_dir, rewritten,
|
||||||
"快照必须看得见已有文件的 mtime 变化,否则零落盘断言是假绿"
|
"快照必须看得见已有文件的 mtime 变化,否则零落盘断言是假绿"
|
||||||
);
|
);
|
||||||
|
// 差异必须是我们推的那一下,而不是两次 `stat` 的偶然抖动(粗粒度文件系统会把 5s 取整到
|
||||||
|
// 4s 或 6s,所以判据是「至少前进 4s」而不是「恰好 5s」)。
|
||||||
|
let after = std::fs::metadata(&manifest)
|
||||||
|
.expect("清单元数据")
|
||||||
|
.modified()
|
||||||
|
.expect("推进后的 mtime");
|
||||||
|
assert!(
|
||||||
|
after.duration_since(before).expect("mtime 应当前进了") >= Duration::from_secs(4),
|
||||||
|
"mtime 应当被推到基线之后:{before:?} → {after:?}"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user