@dsh-gis/dsh-video-studio-skill 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -12
- package/docs/styles/demo-results.json +76 -0
- package/docs/styles/sheet-8-styles.png +0 -0
- package/docs/styles/sheet-ai.png +0 -0
- package/docs/styles/sheet-craft.png +0 -0
- package/docs/styles/sheet-night.png +0 -0
- package/package.json +11 -3
- package/schemas/film.schema.json +528 -0
- package/skills/video-studio/SKILL.md +166 -14
- package/skills/video-studio/assets/film.json +84 -23
- package/skills/video-studio/assets/scenes.py +892 -34
- package/skills/video-studio/references/audio.md +29 -1
- package/skills/video-studio/references/change-impact.md +54 -0
- package/skills/video-studio/references/quality-gates.md +1 -1
- package/skills/video-studio/references/style-packs.md +178 -0
- package/skills/video-studio/requirements.txt +11 -0
- package/skills/video-studio/scripts/filmcheck.py +656 -0
- package/skills/video-studio/scripts/filmkit.py +682 -58
- package/skills/video-studio/scripts/gates.py +446 -0
- package/skills/video-studio/scripts/render.py +562 -34
- package/skills/video-studio/scripts/scorekit.py +1642 -25
- package/skills/video-studio/scripts/selfcheck.py +346 -0
- package/skills/video-studio/scripts/stagewatch.py +295 -0
- package/skills/video-studio/scripts/stylekit.py +924 -0
- package/skills/video-studio/scripts/voicekit.py +872 -0
- package/tools/ci_check.py +339 -0
- package/tools/estimate.py +578 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""ci_check.py — 仓库级体检(不需要 ffmpeg / 字体 / 网络,可在任何机器和 CI 上跑)。
|
|
3
|
+
|
|
4
|
+
跑法:python tools/ci_check.py [-v]
|
|
5
|
+
|
|
6
|
+
它检查的是**这个仓库作为"可分发的技能插件"的完整性**,而不是视频能不能出片
|
|
7
|
+
(出片要靠 selfcheck.py + 真跑一遍 render.py,见 SKILL.md §14):
|
|
8
|
+
|
|
9
|
+
1 语法:所有 .py 能编译、index.js 能通过 node --check
|
|
10
|
+
2 JSON:所有 .json 能被解析(locale / marketplace-entry / assets / package.json)
|
|
11
|
+
3 技能清单:SKILL.md 的 frontmatter 有 name/description,name 与目录名一致
|
|
12
|
+
4 依赖白名单:scripts/*.py 与 assets/*.py 只允许 stdlib + numpy/PIL/cv2/scipy/edge_tts/playwright
|
|
13
|
+
(技能是"整目录拷走就能跑"的,偷偷引入一个三方依赖会在用户机器上炸)
|
|
14
|
+
5 打包卫生:npm pack 的内容里不能有 __pycache__ / *.pyc / 临时目录
|
|
15
|
+
6 文档引用:SKILL.md / references 里引用的 scripts/xxx.py、assets/xxx 必须真的存在
|
|
16
|
+
7 Python 3.9 语法兼容(THIRD_PARTY_NOTICES 写的是 3.9+;这里只验语法,运行期 API 仍要真机实测)
|
|
17
|
+
8 Markdown 围栏成对(不平衡会让整篇渲染成代码块)
|
|
18
|
+
9 GitHub Actions workflow 里 step 名的裸冒号(会让整份 workflow 静默不跑)
|
|
19
|
+
10 体积:单个文件不超过 400 KB(技能要能塞进上下文与包里)
|
|
20
|
+
|
|
21
|
+
退出码:0 全过;1 有 FAIL。每一项都打印证据,不打"可能有问题"。
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import argparse
|
|
26
|
+
import ast
|
|
27
|
+
import json
|
|
28
|
+
import os
|
|
29
|
+
import re
|
|
30
|
+
import subprocess
|
|
31
|
+
import sys
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
|
|
34
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
35
|
+
SKILL = ROOT / "skills" / "video-studio"
|
|
36
|
+
SCRIPTS = SKILL / "scripts"
|
|
37
|
+
ASSETS = SKILL / "assets"
|
|
38
|
+
REFS = SKILL / "references"
|
|
39
|
+
|
|
40
|
+
# 技能允许的三方依赖(与 README「环境前提」一一对应;加东西必须先改文档)
|
|
41
|
+
ALLOWED_THIRD_PARTY = {"numpy", "PIL", "cv2", "scipy", "edge_tts", "playwright"}
|
|
42
|
+
# 同目录同级模块(拷走整个目录后仍能互相 import)
|
|
43
|
+
LOCAL_MODULES = {"filmkit", "sfxkit", "scorekit", "webengine", "qa_frames", "render", "selfcheck",
|
|
44
|
+
"stylekit", "voicekit", "scenes", "scenes_demo",
|
|
45
|
+
# v0.6.0:门禁账本 / 交付报告 / 签核、阶段新鲜度守卫、film.json 契约校验器
|
|
46
|
+
"gates", "stagewatch", "filmcheck"}
|
|
47
|
+
MAX_FILE_BYTES = 400 * 1024
|
|
48
|
+
|
|
49
|
+
_results: list[tuple[bool, str, str]] = []
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def check(name: str, fn):
|
|
53
|
+
"""跑一项检查;返回 (ok, name, detail)。任何异常都算 FAIL 并保留消息。"""
|
|
54
|
+
try:
|
|
55
|
+
detail = fn() or ""
|
|
56
|
+
_results.append((True, name, str(detail)))
|
|
57
|
+
return True
|
|
58
|
+
except AssertionError as e:
|
|
59
|
+
_results.append((False, name, str(e)))
|
|
60
|
+
return False
|
|
61
|
+
except Exception as e: # noqa: BLE001
|
|
62
|
+
_results.append((False, name, "%s: %s" % (type(e).__name__, e)))
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# ---------------------------------------------------------------- 1 语法
|
|
67
|
+
def _py_files() -> list[Path]:
|
|
68
|
+
out = sorted(SCRIPTS.glob("*.py")) + sorted(ASSETS.glob("*.py"))
|
|
69
|
+
return [p for p in out if p.name != "__init__.py"]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def t_syntax_py():
|
|
73
|
+
bad = []
|
|
74
|
+
for p in _py_files():
|
|
75
|
+
src = p.read_text(encoding="utf-8")
|
|
76
|
+
try:
|
|
77
|
+
compile(src, str(p), "exec")
|
|
78
|
+
except SyntaxError as e:
|
|
79
|
+
bad.append("%s:%s %s" % (p.name, e.lineno, e.msg))
|
|
80
|
+
assert not bad, ";".join(bad)
|
|
81
|
+
return "%d 个 .py 全部可编译" % len(_py_files())
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def t_syntax_js():
|
|
85
|
+
node = _which("node")
|
|
86
|
+
if node is None:
|
|
87
|
+
return "跳过(没装 node)"
|
|
88
|
+
js = [p for p in ROOT.glob("*.js")] + sorted((ASSETS / "web").glob("*.js"))
|
|
89
|
+
for p in js:
|
|
90
|
+
r = subprocess.run([node, "--check", str(p)], capture_output=True, text=True)
|
|
91
|
+
assert r.returncode == 0, "%s: %s" % (p.name, (r.stderr or r.stdout).strip()[:300])
|
|
92
|
+
return "%d 个 .js 全部通过 node --check" % len(js)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _which(exe: str):
|
|
96
|
+
from shutil import which
|
|
97
|
+
return which(exe)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# ---------------------------------------------------------------- 2 JSON
|
|
101
|
+
def t_json():
|
|
102
|
+
files = [ROOT / "package.json", ROOT / "marketplace-entry.json"]
|
|
103
|
+
files += sorted((ROOT / "locale").glob("*.json"))
|
|
104
|
+
files += sorted(ASSETS.glob("*.json"))
|
|
105
|
+
for p in files:
|
|
106
|
+
try:
|
|
107
|
+
json.loads(p.read_text(encoding="utf-8"))
|
|
108
|
+
except Exception as e: # noqa: BLE001
|
|
109
|
+
raise AssertionError("%s: %s" % (p.relative_to(ROOT), e))
|
|
110
|
+
return "%d 个 .json 全部可解析" % len(files)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ---------------------------------------------------------------- 3 技能清单
|
|
114
|
+
def t_manifest():
|
|
115
|
+
md = SKILL / "SKILL.md"
|
|
116
|
+
assert md.is_file(), "缺 skills/video-studio/SKILL.md"
|
|
117
|
+
txt = md.read_text(encoding="utf-8")
|
|
118
|
+
assert txt.startswith("---"), "SKILL.md 必须以 YAML frontmatter 开头"
|
|
119
|
+
end = txt.find("\n---", 3)
|
|
120
|
+
assert end > 0, "SKILL.md frontmatter 没有闭合的 ---"
|
|
121
|
+
fm = txt[3:end]
|
|
122
|
+
name = re.search(r"^name:\s*(\S+)\s*$", fm, re.M)
|
|
123
|
+
desc = re.search(r"^description:\s*(.+)$", fm, re.M)
|
|
124
|
+
assert name, "frontmatter 缺 name"
|
|
125
|
+
assert desc and len(desc.group(1).strip()) >= 20, "frontmatter 缺 description 或太短"
|
|
126
|
+
assert name.group(1) == "video-studio", "frontmatter name=%r,应为目录名 video-studio" % name.group(1)
|
|
127
|
+
return "name=video-studio / description %d 字" % len(desc.group(1).strip())
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# ---------------------------------------------------------------- 4 依赖白名单
|
|
131
|
+
def t_deps():
|
|
132
|
+
stdlib = set(sys.stdlib_module_names)
|
|
133
|
+
bad = []
|
|
134
|
+
for p in _py_files():
|
|
135
|
+
tree = ast.parse(p.read_text(encoding="utf-8"))
|
|
136
|
+
for node in ast.walk(tree):
|
|
137
|
+
names = []
|
|
138
|
+
if isinstance(node, ast.Import):
|
|
139
|
+
names = [a.name for a in node.names]
|
|
140
|
+
elif isinstance(node, ast.ImportFrom) and node.level == 0 and node.module:
|
|
141
|
+
names = [node.module]
|
|
142
|
+
for n in names:
|
|
143
|
+
top = n.split(".")[0]
|
|
144
|
+
if top in stdlib or top in ALLOWED_THIRD_PARTY or top in LOCAL_MODULES:
|
|
145
|
+
continue
|
|
146
|
+
bad.append("%s: import %s(不在白名单)" % (p.name, top))
|
|
147
|
+
assert not bad, ";".join(sorted(set(bad)))
|
|
148
|
+
return "白名单:stdlib + %s" % "/".join(sorted(ALLOWED_THIRD_PARTY))
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# ---------------------------------------------------------------- 5 打包卫生
|
|
152
|
+
def t_pack():
|
|
153
|
+
npm = _which("npm")
|
|
154
|
+
if npm is None:
|
|
155
|
+
return "跳过(没装 npm)"
|
|
156
|
+
r = subprocess.run([npm, "pack", "--dry-run", "--json"], cwd=str(ROOT),
|
|
157
|
+
capture_output=True, text=True, shell=(os.name == "nt"))
|
|
158
|
+
assert r.returncode == 0, "npm pack --dry-run 失败:%s" % (r.stderr or r.stdout)[:300]
|
|
159
|
+
try:
|
|
160
|
+
meta = json.loads(r.stdout)[0]
|
|
161
|
+
except Exception as e: # noqa: BLE001
|
|
162
|
+
raise AssertionError("解析 npm pack 输出失败:%s" % e)
|
|
163
|
+
files = [f["path"] for f in meta.get("files", [])]
|
|
164
|
+
# 垃圾文件可能出现在**包内任意位置**(实测踩过:队友的临时目录建在 skills/ 下,
|
|
165
|
+
# 于是 skills/tmp_scratch_x/webtest/*.mp4 被一起打进包,体量翻倍)。
|
|
166
|
+
junk_suffix = (".pyc", ".pyo", ".log", ".tmp", ".orig", ".bak", ".mp4", ".wav", ".mkv", ".tgz")
|
|
167
|
+
junk_parts = ("__pycache__", "tmp_scratch_", "node_modules", ".pytest_cache", ".cordis")
|
|
168
|
+
junk = [f for f in files
|
|
169
|
+
if f.endswith(junk_suffix) or any(p in f for p in junk_parts)
|
|
170
|
+
or f.startswith(("work/", "out/", ".team/"))]
|
|
171
|
+
assert not junk, ("包内不该有这些文件(临时产物/日志/媒体,属于打包污染):%s"
|
|
172
|
+
% ", ".join(junk[:6]))
|
|
173
|
+
must = ["index.js", "cordis.patch.yml", "icon.svg", "package.json",
|
|
174
|
+
"skills/video-studio/SKILL.md", "skills/video-studio/scripts/filmkit.py"]
|
|
175
|
+
missing = [m for m in must if m not in files]
|
|
176
|
+
assert not missing, "包内缺文件:%s" % ", ".join(missing)
|
|
177
|
+
return "%d 个文件 / %.0f KB" % (len(files), meta.get("size", 0) / 1024.0)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# ---------------------------------------------------------------- 6 文档引用
|
|
181
|
+
def t_doc_refs():
|
|
182
|
+
md_files = [SKILL / "SKILL.md", ROOT / "README.md"] + sorted(REFS.glob("*.md"))
|
|
183
|
+
missing = []
|
|
184
|
+
for md in md_files:
|
|
185
|
+
if not md.is_file():
|
|
186
|
+
continue
|
|
187
|
+
txt = md.read_text(encoding="utf-8")
|
|
188
|
+
for m in re.finditer(r"(?:scripts|assets|references)/[A-Za-z0-9_.\-/]+", txt):
|
|
189
|
+
rel = m.group(0).rstrip(".,;:))")
|
|
190
|
+
# 文档里的相对路径有多种写法(<SKILL_DIR>/scripts/x.py、references/y.md),两种都试
|
|
191
|
+
cands = [SKILL / rel, SKILL / "references" / rel, ROOT / rel]
|
|
192
|
+
if not any(c.exists() for c in cands):
|
|
193
|
+
missing.append("%s 里引用 %s(不存在)" % (md.name, rel))
|
|
194
|
+
assert not missing, ";".join(sorted(set(missing))[:8])
|
|
195
|
+
return "%d 篇文档的路径引用全部存在" % sum(1 for m in md_files if m.is_file())
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
# ---------------------------------------------------------------- 7 体积
|
|
199
|
+
def t_size():
|
|
200
|
+
big = []
|
|
201
|
+
for p in [p for p in ROOT.rglob("*") if p.is_file()
|
|
202
|
+
and ".git" not in p.parts and "node_modules" not in p.parts
|
|
203
|
+
and not str(p.relative_to(ROOT)).startswith("tmp_scratch_")
|
|
204
|
+
and p.suffix != ".png"]:
|
|
205
|
+
n = p.stat().st_size
|
|
206
|
+
if n > MAX_FILE_BYTES:
|
|
207
|
+
big.append("%s %.0f KB" % (p.relative_to(ROOT), n / 1024.0))
|
|
208
|
+
assert not big, ";".join(big)
|
|
209
|
+
return "所有文本文件 < %.0f KB" % (MAX_FILE_BYTES / 1024.0)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# ---------------------------------------------------------------- 8 兼容 Python 3.9(README/THIRD_PARTY 的承诺)
|
|
214
|
+
def t_py39():
|
|
215
|
+
bad = []
|
|
216
|
+
for p in _py_files():
|
|
217
|
+
src = p.read_text(encoding="utf-8")
|
|
218
|
+
try:
|
|
219
|
+
ast.parse(src, filename=str(p), feature_version=(3, 9))
|
|
220
|
+
except SyntaxError as e:
|
|
221
|
+
bad.append("%s:%s %s" % (p.name, e.lineno, e.msg))
|
|
222
|
+
assert not bad, ";".join(bad)
|
|
223
|
+
return "%d 个 .py 通过 3.9 语法解析(运行期兼容仍需真 3.9 实测)" % len(_py_files())
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# ---------------------------------------------------------------- 9 Markdown 围栏平衡
|
|
228
|
+
def t_md_fences():
|
|
229
|
+
"""每篇 .md 的 \`\`\` 必须成对:围栏不平衡会让后面整篇渲染成代码块,人眼未必看得出。"""
|
|
230
|
+
bad = []
|
|
231
|
+
files = [ROOT / "README.md", ROOT / "CHANGELOG.md", ROOT / "THIRD_PARTY_NOTICES.md",
|
|
232
|
+
SKILL / "SKILL.md"] + sorted(REFS.glob("*.md"))
|
|
233
|
+
for p in files:
|
|
234
|
+
if not p.is_file():
|
|
235
|
+
continue
|
|
236
|
+
n = sum(1 for line in p.read_text(encoding="utf-8").splitlines()
|
|
237
|
+
if line.lstrip().startswith("\`\`\`"))
|
|
238
|
+
if n % 2:
|
|
239
|
+
bad.append("%s: %d 个围栏(奇数)" % (p.name, n))
|
|
240
|
+
assert not bad, ";".join(bad)
|
|
241
|
+
return "%d 篇 Markdown 围栏都成对" % len([p for p in files if p.is_file()])
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
# ---------------------------------------------------------------- 10 CI workflow 基本语法
|
|
245
|
+
def t_ci_workflow():
|
|
246
|
+
"""GitHub Actions workflow 的「裸冒号」陷阱。
|
|
247
|
+
|
|
248
|
+
「- name: selftest: xxx」里第二个冒号会让整份 YAML 解析失败,而 GitHub 上的表现是
|
|
249
|
+
「workflow 根本没跑」——没有任何报错提示,最容易被忽略(本项目真踩过)。
|
|
250
|
+
不引第三方 YAML 库(CI 里不一定装了 pyyaml),只查这一类高频错误。
|
|
251
|
+
"""
|
|
252
|
+
wf = ROOT / ".github" / "workflows"
|
|
253
|
+
if not wf.is_dir():
|
|
254
|
+
return "跳过(没有 .github/workflows)"
|
|
255
|
+
bad = []
|
|
256
|
+
files = sorted(wf.glob("*.yml")) + sorted(wf.glob("*.yaml"))
|
|
257
|
+
for f in files:
|
|
258
|
+
for i, line in enumerate(f.read_text(encoding="utf-8").splitlines(), 1):
|
|
259
|
+
m = re.match(r"^(\s*)-\s+name:\s*(.+)$", line)
|
|
260
|
+
if not m:
|
|
261
|
+
continue
|
|
262
|
+
val = m.group(2).strip()
|
|
263
|
+
if val[:1] in ("'", '"'):
|
|
264
|
+
continue
|
|
265
|
+
if ": " in val:
|
|
266
|
+
bad.append("%s:%d 的 step 名含未加引号的冒号(YAML 会解析失败,整份 workflow 不跑)"
|
|
267
|
+
% (f.name, i))
|
|
268
|
+
assert not bad, ";".join(bad[:4])
|
|
269
|
+
return "%d 个 workflow 的 step 名没有裸冒号" % len(files)
|
|
270
|
+
|
|
271
|
+
# ---------------------------------------------------------------- 12 film.json 契约
|
|
272
|
+
def t_film_contract():
|
|
273
|
+
"""用技能自带的 filmcheck.py 校验模板,并把 schema 与脚本对齐成回归。
|
|
274
|
+
|
|
275
|
+
为什么要进 CI:film.json 是唯一内容真源,而未知键以前是**静默忽略**的
|
|
276
|
+
(hold 写成 holds、kind 拼错、cue 的 at 指向不存在的句子,全都"跑得通但效果没了")。
|
|
277
|
+
这里不重写校验逻辑,直接调技能里的 filmcheck.py——校验规则只允许有一份。
|
|
278
|
+
"""
|
|
279
|
+
import subprocess
|
|
280
|
+
scripts = ROOT / "skills" / "video-studio" / "scripts"
|
|
281
|
+
film = ROOT / "skills" / "video-studio" / "assets" / "film.json"
|
|
282
|
+
schema = ROOT / "schemas" / "film.schema.json"
|
|
283
|
+
assert schema.exists(), "缺 schemas/film.schema.json(film.json 的契约)"
|
|
284
|
+
try:
|
|
285
|
+
json.loads(schema.read_text(encoding="utf-8"))
|
|
286
|
+
except json.JSONDecodeError as e:
|
|
287
|
+
raise AssertionError("schemas/film.schema.json 不是合法 JSON:%s" % e) from e
|
|
288
|
+
checker = scripts / "filmcheck.py"
|
|
289
|
+
if not checker.exists():
|
|
290
|
+
raise AssertionError("缺 skills/video-studio/scripts/filmcheck.py")
|
|
291
|
+
p = subprocess.run([sys.executable, str(checker), str(film)],
|
|
292
|
+
cwd=str(scripts), capture_output=True, text=True,
|
|
293
|
+
encoding="utf-8", errors="replace", timeout=120)
|
|
294
|
+
out = (p.stdout or "") + (p.stderr or "")
|
|
295
|
+
assert p.returncode == 0, ("模板 film.json 没过契约校验(exit=%d):\n%s"
|
|
296
|
+
% (p.returncode, out.strip()[-600:]))
|
|
297
|
+
return "模板 film.json 通过契约校验;schema %d 字节" % len(schema.read_text(encoding="utf-8"))
|
|
298
|
+
|
|
299
|
+
def main(argv=None) -> int:
|
|
300
|
+
ap = argparse.ArgumentParser(description="仓库级体检(不需要 ffmpeg / 字体 / 网络)")
|
|
301
|
+
ap.add_argument("-v", "--verbose", action="store_true", help="打印每一节的详细证据")
|
|
302
|
+
a = ap.parse_args(argv)
|
|
303
|
+
try:
|
|
304
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
305
|
+
except Exception: # noqa: BLE001
|
|
306
|
+
pass
|
|
307
|
+
|
|
308
|
+
checks = [
|
|
309
|
+
("1 Python 语法", t_syntax_py),
|
|
310
|
+
("2 JS 语法", t_syntax_js),
|
|
311
|
+
("3 JSON 可解析", t_json),
|
|
312
|
+
("4 技能清单 frontmatter", t_manifest),
|
|
313
|
+
("5 三方依赖白名单", t_deps),
|
|
314
|
+
("6 打包卫生(npm pack)", t_pack),
|
|
315
|
+
("7 文档路径引用", t_doc_refs),
|
|
316
|
+
("8 Python 3.9 语法兼容", t_py39),
|
|
317
|
+
("9 Markdown 围栏平衡", t_md_fences),
|
|
318
|
+
("10 CI workflow 语法", t_ci_workflow),
|
|
319
|
+
("11 单文件体积", t_size),
|
|
320
|
+
("12 film.json 契约", t_film_contract),
|
|
321
|
+
]
|
|
322
|
+
print("ci_check %s" % ROOT)
|
|
323
|
+
print("-" * 66)
|
|
324
|
+
for name, fn in checks:
|
|
325
|
+
ok = check(name, fn)
|
|
326
|
+
for ok, name, detail in _results:
|
|
327
|
+
print(" %s %-24s %s" % ("PASS" if ok else "FAIL", name, detail if (ok or a.verbose) else ""))
|
|
328
|
+
fails = [r for r in _results if not r[0]]
|
|
329
|
+
print("-" * 66)
|
|
330
|
+
print("PASS %d / FAIL %d" % (len(_results) - len(fails), len(fails)))
|
|
331
|
+
if fails:
|
|
332
|
+
print("\n失败项:")
|
|
333
|
+
for ok, name, detail in fails:
|
|
334
|
+
print(" - %s:%s" % (name, detail))
|
|
335
|
+
return 1 if fails else 0
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
if __name__ == "__main__":
|
|
339
|
+
sys.exit(main())
|