Merge branch 'main' of https://git.yumee.top/laily/.pouch
This commit is contained in:
Executable
+72
@@ -0,0 +1,72 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Repro: 755 maintainer scripts must pass; 644 must fail as not executable.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
root=$(cd "$(dirname "$0")" && pwd)
|
||||||
|
verify=$root/verify_deb.sh
|
||||||
|
scratch=$(mktemp -d)
|
||||||
|
trap 'rm -rf -- "$scratch"' EXIT
|
||||||
|
|
||||||
|
make_deb_755() {
|
||||||
|
local dest=$1
|
||||||
|
local pkg=$scratch/pkg-755
|
||||||
|
rm -rf "$pkg"
|
||||||
|
mkdir -p "$pkg/DEBIAN" "$pkg/usr/bin"
|
||||||
|
printf '#!/bin/sh\nexit 0\n' >"$pkg/usr/bin/noop"
|
||||||
|
chmod 755 "$pkg/usr/bin/noop"
|
||||||
|
cat >"$pkg/DEBIAN/control" <<'EOF'
|
||||||
|
Package: verify-deb-fixture
|
||||||
|
Version: 0.0.1
|
||||||
|
Architecture: amd64
|
||||||
|
Maintainer: test <test@example.com>
|
||||||
|
Description: fixture for verify_deb.sh permission check
|
||||||
|
EOF
|
||||||
|
printf '#!/bin/sh\nexit 0\n' >"$pkg/DEBIAN/postinst"
|
||||||
|
chmod 755 "$pkg/DEBIAN/postinst"
|
||||||
|
dpkg-deb --build "$pkg" "$dest" >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# dpkg-deb --build rejects 644 maintainer scripts; strip the execute bit in the
|
||||||
|
# control tarball so verify_deb.sh still sees a real packaged 644 postinst.
|
||||||
|
make_deb_644_from() {
|
||||||
|
local src=$1 dest=$2
|
||||||
|
local work=$scratch/mutate
|
||||||
|
rm -rf "$work"
|
||||||
|
mkdir -p "$work/control"
|
||||||
|
cp "$src" "$dest"
|
||||||
|
( cd "$work" && ar x "$dest" )
|
||||||
|
zstd -d -c "$work/control.tar.zst" | tar -C "$work/control" -xf -
|
||||||
|
chmod 644 "$work/control/postinst"
|
||||||
|
tar -C "$work/control" -cf - . | zstd -c >"$work/control.tar.zst"
|
||||||
|
( cd "$work" && ar r "$dest" control.tar.zst )
|
||||||
|
}
|
||||||
|
|
||||||
|
ok_deb=$scratch/verify-deb-fixture_0.0.1_amd64.deb
|
||||||
|
bad_deb=$scratch/verify-deb-fixture-noexec_0.0.1_amd64.deb
|
||||||
|
make_deb_755 "$ok_deb"
|
||||||
|
make_deb_644_from "$ok_deb" "$bad_deb"
|
||||||
|
|
||||||
|
ok_out=$scratch/ok.out
|
||||||
|
bad_out=$scratch/bad.out
|
||||||
|
set +e
|
||||||
|
"$verify" "$ok_deb" 0.0.1 amd64 >"$ok_out" 2>&1
|
||||||
|
ok_rc=$?
|
||||||
|
"$verify" "$bad_deb" 0.0.1 amd64 >"$bad_out" 2>&1
|
||||||
|
bad_rc=$?
|
||||||
|
set -e
|
||||||
|
|
||||||
|
fail=0
|
||||||
|
if [[ $ok_rc -ne 0 ]] || ! grep -q 'VERIFY: OK' "$ok_out"; then
|
||||||
|
echo "FAIL: 755 postinst should be treated as executable" >&2
|
||||||
|
cat "$ok_out" >&2
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
if [[ $bad_rc -eq 0 ]] || ! grep -q 'not executable' "$bad_out"; then
|
||||||
|
echo "FAIL: 644 postinst should fail as not executable" >&2
|
||||||
|
cat "$bad_out" >&2
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
if ((fail)); then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "test_verify_deb.sh: OK"
|
||||||
@@ -87,8 +87,10 @@ if dpkg-deb --control "$package" "$control_dir" 2>/dev/null; then
|
|||||||
for script in preinst postinst prerm postrm; do
|
for script in preinst postinst prerm postrm; do
|
||||||
if [[ -f "$control_dir/$script" ]]; then
|
if [[ -f "$control_dir/$script" ]]; then
|
||||||
found_scripts=true
|
found_scripts=true
|
||||||
mode=$(stat -c '%a' "$control_dir/$script")
|
# Use -x: octal last digit [2367] is "other write", so 755 (r-x) was
|
||||||
if [[ $mode =~ .*[2367]$ ]]; then
|
# reported as not executable. 644 still fails because it is not -x.
|
||||||
|
mode=$(stat -c '%a' "$control_dir/$script" 2>/dev/null || stat -f '%OLp' "$control_dir/$script")
|
||||||
|
if [[ -x "$control_dir/$script" ]]; then
|
||||||
echo "OK: $script mode $mode"
|
echo "OK: $script mode $mode"
|
||||||
else
|
else
|
||||||
echo "FAIL: $script not executable (mode $mode)" >&2
|
echo "FAIL: $script not executable (mode $mode)" >&2
|
||||||
|
|||||||
@@ -87,4 +87,4 @@ python3 <skill-dir>/scripts/catalog.py show --root <project-root> --id <CASE_ID>
|
|||||||
|
|
||||||
## 运行
|
## 运行
|
||||||
|
|
||||||
读 [run.md](references/run.md)。先 `select` 得到命中列表,再按 surface 分流。未指定范围时等同「suite=smoke 且风险上限=默认」。
|
读 [run.md](references/run.md)。先 `select` 得到命中列表,再按 surface 分流。未指定范围时等同「suite=smoke 且风险上限=默认」。`select` 的 `run_together` 组按 [run.md](references/run.md) 整组执行。
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ python3 <skill-dir>/scripts/catalog.py select --root <root> \
|
|||||||
|
|
||||||
`select` 已排除 `retired` 和风险高于上限的条目(后者若用户点了具体 id,会在 JSON 的 `skipped` 里说明,不执行)。
|
`select` 已排除 `retired` 和风险高于上限的条目(后者若用户点了具体 id,会在 JSON 的 `skipped` 里说明,不执行)。
|
||||||
|
|
||||||
|
若 catalog 有 `run_together`,命中组内任一条就把整组按 `ids` 顺序扩进列表(JSON 带 `run_together`)。这不是擅自扩大范围。
|
||||||
|
|
||||||
无命中:停下说明筛选条件,不要扩大范围。
|
无命中:停下说明筛选条件,不要扩大范围。
|
||||||
|
|
||||||
## 2. 运行前闸
|
## 2. 运行前闸
|
||||||
@@ -37,6 +39,8 @@ python3 <skill-dir>/scripts/catalog.py select --root <root> \
|
|||||||
|
|
||||||
按 select 返回的顺序,一条一条来。select 按 `catalog.yaml` 的 `categories` 顺序,同一分类内按该 yaml 列表顺序(不是文件名字母序)。默认失败即停(产品 `fail` 或探测到的不安全状态)。用户说继续 / keep-going 才往后跑。
|
按 select 返回的顺序,一条一条来。select 按 `catalog.yaml` 的 `categories` 顺序,同一分类内按该 yaml 列表顺序(不是文件名字母序)。默认失败即停(产品 `fail` 或探测到的不安全状态)。用户说继续 / keep-going 才往后跑。
|
||||||
|
|
||||||
|
例外:当前条和下一条同属 `run_together` 且该组 `continue_on_fail: true` 时,前一条产品 `fail` 仍跑后一条。`environment-fail` 仍整轮停。
|
||||||
|
|
||||||
**script**
|
**script**
|
||||||
|
|
||||||
有 `how.run`:
|
有 `how.run`:
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ runs/*.csv 同一次运行的成绩单;generate_run_csv.py 从对
|
|||||||
| `gates` | 可选。项目细分闸名 → 环境变量。用在用例的 `gate` 上,不要拿它代替 `risk` |
|
| `gates` | 可选。项目细分闸名 → 环境变量。用在用例的 `gate` 上,不要拿它代替 `risk` |
|
||||||
| `sessions` | 可复用会话。`kind`: `browser` / `mobile`;`reuse`: `run`(本轮结束关掉)或 `persist`(probe 活着就附着) |
|
| `sessions` | 可复用会话。`kind`: `browser` / `mobile`;`reuse`: `run`(本轮结束关掉)或 `persist`(probe 活着就附着) |
|
||||||
| `id_pattern` | 用例 id 正则 |
|
| `id_pattern` | 用例 id 正则 |
|
||||||
|
| `run_together` | 可选。成组执行:`[{ids: [TC-…, TC-…], continue_on_fail: true}]`。点到组内任一条,`select` 会按 `ids` 顺序把整组扩进命中列表。`continue_on_fail: true` 时组内前一条产品 fail 仍跑后一条(返回/收尾)。一个 id 只能属于一组。 |
|
||||||
|
|
||||||
`local.yaml` 与环境变量覆盖 `HOST` 和授权开关。查找顺序:环境变量 > `local.yaml` > `catalog.yaml` 的 vars。也认 `HX_TEST_HOST` 作为 `HOST`。
|
`local.yaml` 与环境变量覆盖 `HOST` 和授权开关。查找顺序:环境变量 > `local.yaml` > `catalog.yaml` 的 vars。也认 `HX_TEST_HOST` 作为 `HOST`。
|
||||||
|
|
||||||
|
|||||||
@@ -264,6 +264,93 @@ def parse_ids(values: list[str]) -> list[str]:
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def public_case(case: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
return {k: v for k, v in case.items() if not str(k).startswith("_")}
|
||||||
|
|
||||||
|
|
||||||
|
def run_together_groups(meta: dict[str, Any]) -> list[dict[str, Any]]:
|
||||||
|
raw = meta.get("run_together") or []
|
||||||
|
if not isinstance(raw, list):
|
||||||
|
return []
|
||||||
|
groups: list[dict[str, Any]] = []
|
||||||
|
for item in raw:
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
continue
|
||||||
|
ids = [str(x) for x in (item.get("ids") or []) if str(x).strip()]
|
||||||
|
if len(ids) < 2:
|
||||||
|
continue
|
||||||
|
groups.append(
|
||||||
|
{
|
||||||
|
"ids": ids,
|
||||||
|
"continue_on_fail": bool(item.get("continue_on_fail")),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return groups
|
||||||
|
|
||||||
|
|
||||||
|
def expand_run_together(
|
||||||
|
meta: dict[str, Any],
|
||||||
|
all_cases: list[dict[str, Any]],
|
||||||
|
selected: list[dict[str, Any]],
|
||||||
|
skipped: list[dict[str, str]],
|
||||||
|
max_i: int,
|
||||||
|
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
||||||
|
groups = run_together_groups(meta)
|
||||||
|
if not groups:
|
||||||
|
return selected, []
|
||||||
|
by_id = {str(c.get("id") or ""): c for c in all_cases}
|
||||||
|
selected_ids = [str(c.get("id") or "") for c in selected]
|
||||||
|
selected_map = {str(c.get("id") or ""): c for c in selected}
|
||||||
|
hit_groups: list[dict[str, Any]] = []
|
||||||
|
|
||||||
|
for group in groups:
|
||||||
|
ids = group["ids"]
|
||||||
|
if not any(cid in selected_map for cid in ids):
|
||||||
|
continue
|
||||||
|
hit_groups.append(group)
|
||||||
|
first_pos = min(
|
||||||
|
(selected_ids.index(cid) for cid in ids if cid in selected_ids),
|
||||||
|
default=len(selected_ids),
|
||||||
|
)
|
||||||
|
members: list[dict[str, Any]] = []
|
||||||
|
for cid in ids:
|
||||||
|
if cid in selected_map:
|
||||||
|
members.append(selected_map[cid])
|
||||||
|
continue
|
||||||
|
case = by_id.get(cid)
|
||||||
|
if not case or case.get("status", "active") != "active":
|
||||||
|
skipped.append({"id": cid, "reason": "run_together 搭档不是 active"})
|
||||||
|
continue
|
||||||
|
risk = str(case.get("risk") or "safe")
|
||||||
|
try:
|
||||||
|
r_i = risk_index(meta, risk)
|
||||||
|
except SystemExit:
|
||||||
|
skipped.append({"id": cid, "reason": f"未知风险 {risk}"})
|
||||||
|
continue
|
||||||
|
if r_i > max_i:
|
||||||
|
skipped.append({"id": cid, "reason": f"risk={risk} 超过上限"})
|
||||||
|
continue
|
||||||
|
members.append(public_case(case))
|
||||||
|
drop = set(ids)
|
||||||
|
new_selected: list[dict[str, Any]] = []
|
||||||
|
inserted = False
|
||||||
|
for case in selected:
|
||||||
|
cid = str(case.get("id") or "")
|
||||||
|
if cid in drop:
|
||||||
|
if not inserted:
|
||||||
|
new_selected.extend(members)
|
||||||
|
inserted = True
|
||||||
|
continue
|
||||||
|
new_selected.append(case)
|
||||||
|
if not inserted:
|
||||||
|
new_selected.extend(members)
|
||||||
|
selected = new_selected
|
||||||
|
selected_ids = [str(c.get("id") or "") for c in selected]
|
||||||
|
selected_map = {str(c.get("id") or ""): c for c in selected}
|
||||||
|
|
||||||
|
return selected, hit_groups
|
||||||
|
|
||||||
|
|
||||||
def cmd_select(args: argparse.Namespace, root: Path) -> int:
|
def cmd_select(args: argparse.Namespace, root: Path) -> int:
|
||||||
meta = load_catalog_meta(root)
|
meta = load_catalog_meta(root)
|
||||||
cases = load_all_cases(root)
|
cases = load_all_cases(root)
|
||||||
@@ -305,8 +392,7 @@ def cmd_select(args: argparse.Namespace, root: Path) -> int:
|
|||||||
if r_i > max_i:
|
if r_i > max_i:
|
||||||
skipped.append({"id": cid, "reason": f"risk={risk} 超过上限 {max_risk}"})
|
skipped.append({"id": cid, "reason": f"risk={risk} 超过上限 {max_risk}"})
|
||||||
continue
|
continue
|
||||||
public = {k: v for k, v in case.items() if not str(k).startswith("_")}
|
selected.append(public_case(case))
|
||||||
selected.append(public)
|
|
||||||
|
|
||||||
if wanted and missing:
|
if wanted and missing:
|
||||||
print(
|
print(
|
||||||
@@ -315,12 +401,15 @@ def cmd_select(args: argparse.Namespace, root: Path) -> int:
|
|||||||
)
|
)
|
||||||
return 2
|
return 2
|
||||||
|
|
||||||
|
selected, together = expand_run_together(meta, cases, selected, skipped, max_i)
|
||||||
|
|
||||||
print(
|
print(
|
||||||
json.dumps(
|
json.dumps(
|
||||||
{
|
{
|
||||||
"max_risk": max_risk,
|
"max_risk": max_risk,
|
||||||
"count": len(selected),
|
"count": len(selected),
|
||||||
"skipped": skipped,
|
"skipped": skipped,
|
||||||
|
"run_together": together,
|
||||||
"cases": selected,
|
"cases": selected,
|
||||||
},
|
},
|
||||||
ensure_ascii=False,
|
ensure_ascii=False,
|
||||||
@@ -405,6 +494,28 @@ def validate(root: Path) -> list[str]:
|
|||||||
except SystemExit as exc:
|
except SystemExit as exc:
|
||||||
return errors + [str(exc)]
|
return errors + [str(exc)]
|
||||||
|
|
||||||
|
together_raw = meta.get("run_together")
|
||||||
|
if together_raw is None:
|
||||||
|
together_raw = []
|
||||||
|
if not isinstance(together_raw, list):
|
||||||
|
errors.append("catalog.run_together 必须是列表")
|
||||||
|
together_raw = []
|
||||||
|
together_seen: set[str] = set()
|
||||||
|
for i, item in enumerate(together_raw):
|
||||||
|
where = f"catalog.run_together[{i}]"
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
errors.append(f"{where}: 必须是 mapping,含 ids")
|
||||||
|
continue
|
||||||
|
ids = item.get("ids")
|
||||||
|
if not isinstance(ids, list) or len(ids) < 2:
|
||||||
|
errors.append(f"{where}: ids 至少两个")
|
||||||
|
continue
|
||||||
|
for cid in ids:
|
||||||
|
cid_s = str(cid)
|
||||||
|
if cid_s in together_seen:
|
||||||
|
errors.append(f"{where}: id {cid_s} 已在另一组 run_together")
|
||||||
|
together_seen.add(cid_s)
|
||||||
|
|
||||||
for case in cases:
|
for case in cases:
|
||||||
cid = str(case.get("id") or "")
|
cid = str(case.get("id") or "")
|
||||||
where = f"{case.get('_file')}:{cid or '?'}"
|
where = f"{case.get('_file')}:{cid or '?'}"
|
||||||
@@ -416,6 +527,8 @@ def validate(root: Path) -> list[str]:
|
|||||||
if cid in seen:
|
if cid in seen:
|
||||||
errors.append(f"{where}: id 重复(也出现在 {seen[cid]})")
|
errors.append(f"{where}: id 重复(也出现在 {seen[cid]})")
|
||||||
seen[cid] = str(case.get("_file"))
|
seen[cid] = str(case.get("_file"))
|
||||||
|
if cid in together_seen:
|
||||||
|
together_seen.discard(cid)
|
||||||
cat = str(case.get("category") or "")
|
cat = str(case.get("category") or "")
|
||||||
file_cat = str(case.get("_file_category") or "")
|
file_cat = str(case.get("_file_category") or "")
|
||||||
if file_cat and cat and cat != file_cat:
|
if file_cat and cat and cat != file_cat:
|
||||||
@@ -471,6 +584,8 @@ def validate(root: Path) -> list[str]:
|
|||||||
errors.append(f"{where}: expected[{j}] 需要 kind 与 value")
|
errors.append(f"{where}: expected[{j}] 需要 kind 与 value")
|
||||||
elif sig.get("kind") not in EXPECTED_KINDS:
|
elif sig.get("kind") not in EXPECTED_KINDS:
|
||||||
errors.append(f"{where}: expected[{j}].kind 非法")
|
errors.append(f"{where}: expected[{j}].kind 非法")
|
||||||
|
if together_seen:
|
||||||
|
errors.append("catalog.run_together 引用了不存在的 id: " + ", ".join(sorted(together_seen)))
|
||||||
return errors
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user