已开启
fix: 修复 check-json/check-yaml 误杀合法文件及错误报告问题 #2
fix: 修复 check-json/check-yaml 误杀合法文件及错误报告问题 #2
已开启
liruichen创建于 6月25日
共 5 个文件变更+240-11
@@ -91,22 +91,25 @@
91 additional_dependencies: ['autopep8==2.3.1']91 additional_dependencies: ['autopep8==2.3.1']
92 minimum_pre_commit_version: '2.9.2'92 minimum_pre_commit_version: '2.9.2'
93 93 
94-# === JSON / YAML 语法检查(inline entry,无脚本) ===94+# === JSON / YAML 语法检查 ===
95-# 参考 litebmc/pre-commit-hooks:entry 直接内联 python -c,避免新增脚本文件。95+# check-json 落地为 hooks/check_json.py(language: script,纯标准库):可剥除
96-# check-yaml 依赖 pyyaml,由 additional_dependencies 自动安装;check-json 仅用标准库。96+# UTF-8 BOM、逐文件报告错误并在首个坏文件后继续检查(对齐 check-sr 范式)。
97+# check-yaml 依赖 pyyaml,故保持 inline entry + language: python,由
98+# additional_dependencies 自动安装;safe_load_all 支持多文档 YAML,open 'rb'
99+# 交由 PyYAML 做编码自检/剥 BOM,且解析错误消息自带文件名。
97 100 
98- id: check-json101- id: check-json
99 name: Check JSON102 name: Check JSON
100- description: JSON 语法检查103+ description: JSON 语法检查(自动剥除 UTF-8 BOM,逐文件报告错误)
101- entry: python -c "import sys, json; [json.load(open(f, encoding='utf-8')) for f in sys.argv[1:]]"104+ entry: hooks/check_json.py
102- language: python105+ language: script
103 files: \.json$106 files: \.json$
104- minimum_pre_commit_version: '2.9.2'107+ minimum_pre_commit_version: '3.0.0'
105 108 
106- id: check-yaml109- id: check-yaml
107 name: Check YAML110 name: Check YAML
108- description: YAML 语法检查 (yaml.safe_load)111+ description: YAML 语法检查(safe_load_all 支持多文档,自动剥除 BOM)
109- entry: python -c "import sys, yaml; [yaml.safe_load(open(f, encoding='utf-8')) for f in sys.argv[1:]]"112+ entry: python -c "import sys, yaml; [list(yaml.safe_load_all(open(f, 'rb'))) for f in sys.argv[1:]]"
110 language: python113 language: python
111 files: \.(yaml|yml)$114 files: \.(yaml|yml)$
112 additional_dependencies: ['pyyaml==6.0.2']115 additional_dependencies: ['pyyaml==6.0.2']
@@ -19,8 +19,8 @@ openUBMC 专属钩子(conventional-commit / add-signoff-and-change-id / check-
19| conventional-commit | 校验提交信息符合 Conventional Commits 规范 | commit-msg | 否 |19| conventional-commit | 校验提交信息符合 Conventional Commits 规范 | commit-msg | 否 |
20| add-signoff-and-change-id | 在 commit message 末尾追加 Signed-off-by 和 Change-Id | commit-msg | 是 |20| add-signoff-and-change-id | 在 commit message 末尾追加 Signed-off-by 和 Change-Id | commit-msg | 是 |
21| check-sr | 校验 .sr 文件语法(支持 C 风格注释) | pre-commit | 否 |21| check-sr | 校验 .sr 文件语法(支持 C 风格注释) | pre-commit | 否 |
22-| check-json | JSON 语法检查(标准库) | pre-commit | 否 |22+| check-json | JSON 语法检查(标准库,剥 BOM、逐文件报告) | pre-commit | 否 |
23-| check-yaml | YAML 语法检查(pyyaml) | pre-commit | 否 |23+| check-yaml | YAML 语法检查(pyyaml,支持多文档、剥 BOM) | pre-commit | 否 |
24| black | Python 代码格式化检查(line-length=120) | pre-commit | 否 |24| black | Python 代码格式化检查(line-length=120) | pre-commit | 否 |
25| isort | Python import 排序检查(profile=black) | pre-commit | 否 |25| isort | Python import 排序检查(profile=black) | pre-commit | 否 |
26| flake8 | Python 代码风格检查(忽略 E203/W503) | pre-commit | 否 |26| flake8 | Python 代码风格检查(忽略 E203/W503) | pre-commit | 否 |
@@ -0,0 +1,48 @@
1+#!/usr/bin/env python3
2+# encoding=utf-8
3+# 描述:校验 JSON 文件语法(自动剥除 UTF-8 BOM)
4+# Copyright (c) 2024 Huawei Technologies Co., Ltd.
5+# openUBMC is licensed under Mulan PSL v2.
6+# You can use this software according to the terms and conditions of the Mulan PSL v2.
7+# You may obtain a copy of Mulan PSL v2 at:
8+# http://license.coscl.org.cn/MulanPSL2
9+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
10+# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
11+# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
12+# See the Mulan PSL v2 for more details.
13+"""
14+Validate JSON files.
15+ 
16+Reads each file as UTF-8 (a leading BOM is auto-stripped via utf-8-sig) and
17+parses it with ``json.load``. Pure standard library — no third-party deps.
18+ 
19+Relative to a naive inline ``json.load(open(f))`` this hook:
20+- tolerates a leading UTF-8 BOM (valid per RFC 8259 §8.1),
21+- prints the offending file path on error (``<path>: <reason>``),
22+- keeps checking the remaining files instead of bailing on the first failure.
23+ 
24+Usage: check_json.py <file1.json> [file2.json ...]
25+"""
26+import json
27+import sys
28+from typing import Sequence
29+ 
30+ 
31+def main(argv: Sequence[str] | None = None) -> int:
32+ if argv is None:
33+ argv = sys.argv[1:]
34+ if not argv:
35+ return 0
36+ retval = 0
37+ for path in argv:
38+ try:
39+ with open(path, encoding="utf-8-sig") as f:
40+ json.load(f)
41+ except (OSError, ValueError) as e:
42+ print(f"{path}: {e}")
43+ retval = 1
44+ return retval
45+ 
46+ 
47+if __name__ == "__main__":
48+ raise SystemExit(main())
@@ -0,0 +1,91 @@
1+#!/usr/bin/env python3
2+# encoding=utf-8
3+# 描述:check_json.py 钩子的单元测试
4+# Copyright (c) 2024 Huawei Technologies Co., Ltd.
5+# openUBMC is licensed under Mulan PSL v2.
6+# You can use this software according to the terms and conditions of the Mulan PSL v2.
7+# You may obtain a copy of Mulan PSL v2 at:
8+# http://license.coscl.org.cn/MulanPSL2
9+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
10+# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
11+# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
12+# See the Mulan PSL v2 for more details.
13+from __future__ import annotations
14+ 
15+import pytest
16+ 
17+from hooks.check_json import main
18+ 
19+ 
20+@pytest.mark.parametrize(
21+ ("content", "expected_retval"),
22+ (
23+ # --- 合法(retval=0)---
24+ ('{"a": 1}\n', 0), # 纯对象
25+ ('{"a": 1, "b": [2, 3]}\n', 0), # 嵌套数组
26+ ('[1, 2, 3]\n', 0), # 顶层数组
27+ (' {\n "a": 1\n}\n', 0), # 带缩进/换行/空白
28+ ('{"url": "https://example.com"}\n', 0), # 值内的 //(无注释语义,正常)
29+ ('{"name": "中文"}\n', 0), # 非 ASCII(UTF-8 直接编码)
30+ ('"just a string"\n', 0), # 顶层字符串
31+ ('123\n', 0), # 顶层数字
32+ ('null\n', 0), # 顶层 null
33+ # --- 非法(retval=1)---
34+ ('{"a": 1,}\n', 1), # 尾逗号
35+ ("{'a': 1}\n", 1), # 单引号
36+ ("{a: 1}\n", 1), # 裸 key
37+ ('{"a": }\n', 1), # 缺值
38+ ('{"a": 1\n', 1), # 未闭合 }
39+ ("", 1), # 空文件
40+ ('{"a": "hello}\n', 1), # 未闭合字符串
41+ ),
42+)
43+def test_main(tmpdir, capsys, content, expected_retval):
44+ f = tmpdir.join("t.json")
45+ # encode explicitly so Windows (GBK default) doesn't mangle non-ASCII test data
46+ f.write_binary(content.encode("utf-8"))
47+ ret = main((str(f),))
48+ assert ret == expected_retval
49+ if expected_retval == 1:
50+ stdout, _ = capsys.readouterr()
51+ assert str(f) in stdout
52+ 
53+ 
54+def test_main_bom(tmpdir):
55+ """UTF-8 BOM 应被 utf-8-sig 自动剥除,校验通过(Bug 1 回归)。"""
56+ f = tmpdir.join("bom.json")
57+ f.write_binary(b'\xef\xbb\xbf{"a": 1}\n')
58+ assert main((str(f),)) == 0
59+ 
60+ 
61+def test_main_unicode_content(tmpdir):
62+ """非 ASCII 内容(直接 UTF-8 编码,非转义)。"""
63+ f = tmpdir.join("u.json")
64+ f.write_binary('{"name": "中文"}\n'.encode("utf-8"))
65+ assert main((str(f),)) == 0
66+ 
67+ 
68+def test_main_nonexistent_file(capsys):
69+ """不存在的文件:OSError 被捕获,retval=1,文件名出现在输出。"""
70+ ret = main(["/no/such/file.json"])
71+ assert ret == 1
72+ stdout, _ = capsys.readouterr()
73+ assert "/no/such/file.json" in stdout
74+ 
75+ 
76+def test_main_no_args():
77+ """空 argv:直接返回 0,不抛错。"""
78+ assert main([]) == 0
79+ 
80+ 
81+def test_main_multiple_files_one_bad(tmpdir, capsys):
82+ """多文件混合:坏文件不阻断后续检查,整体 retval=1,坏文件名出现、好文件名不出现(问题 3)。"""
83+ good = tmpdir.join("good.json")
84+ good.write('{"a": 1}\n')
85+ bad = tmpdir.join("bad.json")
86+ bad.write('{"a": ,}\n')
87+ ret = main([str(good), str(bad)])
88+ assert ret == 1
89+ stdout, _ = capsys.readouterr()
90+ assert str(bad) in stdout
91+ assert str(good) not in stdout
@@ -0,0 +1,87 @@
1+#!/usr/bin/env python3
2+# encoding=utf-8
3+# 描述:check-yaml 钩子的单元测试(通过 subprocess 执行 inline entry)
4+# Copyright (c) 2024 Huawei Technologies Co., Ltd.
5+# openUBMC is licensed under Mulan PSL v2.
6+# You can use this software according to the terms and conditions of the Mulan PSL v2.
7+# You may obtain a copy of Mulan PSL v2 at:
8+# http://license.coscl.org.cn/MulanPSL2
9+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
10+# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
11+# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
12+# See the Mulan PSL v2 for more details.
13+from __future__ import annotations
14+ 
15+import os
16+import subprocess
17+import sys
18+ 
19+import pytest
20+ 
21+# check-yaml 依赖 pyyaml:测试环境若未安装则整体跳过(requirements-dev 仅含 pytest)。
22+pytest.importorskip("yaml")
23+ 
24+# 必须与 .pre-commit-hooks.yaml 中 check-yaml 的 entry 代码保持一致。
25+CODE = "import sys, yaml; [list(yaml.safe_load_all(open(f, 'rb'))) for f in sys.argv[1:]]"
26+ 
27+ 
28+def run(*files: str) -> subprocess.CompletedProcess:
29+ """以 [python, -c, CODE, *files] 执行 inline entry,等价于 pre-commit 的调用方式。"""
30+ return subprocess.run(
31+ [sys.executable, "-c", CODE, *files],
32+ capture_output=True,
33+ text=True,
34+ )
35+ 
36+ 
37+def _write(tmpdir, name: str, content: str):
38+ f = tmpdir.join(name)
39+ f.write_binary(content.encode("utf-8"))
40+ return str(f)
41+ 
42+ 
43+@pytest.mark.parametrize(
44+ ("content", "should_pass"),
45+ (
46+ ("a: 1\n", True), # 单文档
47+ ("a: 1\n---\nb: 2\n", True), # 合法多文档(Bug 2 回归:旧实现会误报)
48+ ("{\"a\": 1}\n", True), # flow mapping
49+ ("- a\n- b\n", True), # 序列
50+ ("a: [unclosed\n", False), # 语法错误
51+ ("a: 1\n---\nb: [unclosed", False), # 多文档,第 2 个文档语法错误(旧实现根本不检查)
52+ ("key: value: oops\n", False), # 映射值非法
53+ ),
54+)
55+def test_entry(tmpdir, content, should_pass):
56+ f = _write(tmpdir, "t.yaml", content)
57+ result = run(f)
58+ assert (result.returncode == 0) is should_pass
59+ if not should_pass:
60+ # 问题 3:错误消息自带文件名
61+ assert f in (result.stdout + result.stderr)
62+ 
63+ 
64+def test_entry_bom(tmpdir):
65+ """UTF-8 BOM 应被 PyYAML 自动剥除,校验通过。"""
66+ f = tmpdir.join("bom.yaml")
67+ f.write_binary(b"\xef\xbb\xbfkey: val\n")
68+ assert run(str(f)).returncode == 0
69+ 
70+ 
71+def test_entry_nonexistent_file(tmpdir):
72+ """不存在的文件:非零退出,错误自带文件名。"""
73+ missing = str(tmpdir.join("nope.yaml"))
74+ result = run(missing)
75+ assert result.returncode != 0
76+ # Python 3.12+ 的 FileNotFoundError 在 traceback 中用 repr 显示文件名(反斜杠转义),
77+ # 故以 basename 断言文件名可见,与实际 pre-commit 输出一致。
78+ assert os.path.basename(missing) in (result.stdout + result.stderr)
79+ 
80+ 
81+def test_entry_multiple_files_one_bad(tmpdir):
82+ """多文件混合:坏文件触发非零退出且文件名出现在输出。"""
83+ good = _write(tmpdir, "good.yaml", "a: 1\n")
84+ bad = _write(tmpdir, "bad.yaml", "a: [unclosed\n")
85+ result = run(good, bad)
86+ assert result.returncode != 0
87+ assert bad in (result.stdout + result.stderr)