ht32-code-mcp 0.4.3__tar.gz → 0.4.4__tar.gz
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.
- {ht32_code_mcp-0.4.3 → ht32_code_mcp-0.4.4}/PKG-INFO +1 -1
- {ht32_code_mcp-0.4.3 → ht32_code_mcp-0.4.4}/pyproject.toml +1 -1
- {ht32_code_mcp-0.4.3 → ht32_code_mcp-0.4.4}/src/ht32_code_mcp/materializer.py +90 -13
- ht32_code_mcp-0.4.4/tests/test_export_cloud_mode.py +176 -0
- {ht32_code_mcp-0.4.3 → ht32_code_mcp-0.4.4}/tests/test_tools.py +4 -2
- {ht32_code_mcp-0.4.3 → ht32_code_mcp-0.4.4}/uv.lock +1 -1
- ht32_code_mcp-0.4.3/tests/test_export_cloud_mode.py +0 -83
- {ht32_code_mcp-0.4.3 → ht32_code_mcp-0.4.4}/.gitignore +0 -0
- {ht32_code_mcp-0.4.3 → ht32_code_mcp-0.4.4}/.gitkeep +0 -0
- {ht32_code_mcp-0.4.3 → ht32_code_mcp-0.4.4}/.pytest_base_final_run.log +0 -0
- {ht32_code_mcp-0.4.3 → ht32_code_mcp-0.4.4}/DEPLOY.md +0 -0
- {ht32_code_mcp-0.4.3 → ht32_code_mcp-0.4.4}/README.md +0 -0
- {ht32_code_mcp-0.4.3 → ht32_code_mcp-0.4.4}/e2e_scenario_a.py +0 -0
- {ht32_code_mcp-0.4.3 → ht32_code_mcp-0.4.4}/e2e_scenario_b.py +0 -0
- {ht32_code_mcp-0.4.3 → ht32_code_mcp-0.4.4}/smoke_http.py +0 -0
- {ht32_code_mcp-0.4.3 → ht32_code_mcp-0.4.4}/smoke_stdio.py +0 -0
- {ht32_code_mcp-0.4.3 → ht32_code_mcp-0.4.4}/src/ht32_code_mcp/__init__.py +0 -0
- {ht32_code_mcp-0.4.3 → ht32_code_mcp-0.4.4}/src/ht32_code_mcp/__main__.py +0 -0
- {ht32_code_mcp-0.4.3 → ht32_code_mcp-0.4.4}/src/ht32_code_mcp/exporter.py +0 -0
- {ht32_code_mcp-0.4.3 → ht32_code_mcp-0.4.4}/src/ht32_code_mcp/index.py +0 -0
- {ht32_code_mcp-0.4.3 → ht32_code_mcp-0.4.4}/src/ht32_code_mcp/knowledge.py +0 -0
- {ht32_code_mcp-0.4.3 → ht32_code_mcp-0.4.4}/src/ht32_code_mcp/release.py +0 -0
- {ht32_code_mcp-0.4.3 → ht32_code_mcp-0.4.4}/src/ht32_code_mcp/server.py +0 -0
- {ht32_code_mcp-0.4.3 → ht32_code_mcp-0.4.4}/src/ht32_code_mcp/usage.py +0 -0
- {ht32_code_mcp-0.4.3 → ht32_code_mcp-0.4.4}/tests/conftest.py +0 -0
- {ht32_code_mcp-0.4.3 → ht32_code_mcp-0.4.4}/tests/test_knowledge_grounding.py +0 -0
- {ht32_code_mcp-0.4.3 → ht32_code_mcp-0.4.4}/tests/test_recipe_claims.py +0 -0
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
1. 先校验(schema/来源 sha256/授权/唯一 recipe/配置闭包)再创建 staging;
|
|
5
5
|
2. XML 只做限定 target/节点路径/预期命中数的声明式改写,保留原字节其余部分;
|
|
6
6
|
3. 内存一处裁决(recipe memory model)、多处一致:Cpu/IROM/IRAM/OCR_RVCT4/OCR_RVCT9/
|
|
7
|
-
|
|
7
|
+
FlashDriverDll -FL 由同一模型派生;umfTarg 保持官方生成工程口径(0);
|
|
8
8
|
4. manifest 四件套(project-lock/files/deviations/validation.json)随产物发布;
|
|
9
9
|
5. staging -> 同文件系统原子重命名;目标已存在则失败,不合并覆盖;
|
|
10
10
|
6. ZIP 写 .part 校验后原子发布;失败不留公开半成品。
|
|
@@ -622,6 +622,14 @@ class Materializer:
|
|
|
622
622
|
lib_src = pkg / ("libraries" if kernel == "m4" else "library")
|
|
623
623
|
lib_dst = stage / "library"
|
|
624
624
|
self._copy_official_tree(lib_src, lib_dst, manifest_files, origin["package"])
|
|
625
|
+
# 1b) middlewares:仅 M4 官方包自带(USB drivers/classes、FatFS、I2C app library)。
|
|
626
|
+
# 官方工程 IncludePath 直接引用 middlewares\usb_drivers\inc,必须随工程携带;
|
|
627
|
+
# M0+/M3 官方包无此目录。
|
|
628
|
+
if kernel == "m4":
|
|
629
|
+
mid_src = pkg / "middlewares"
|
|
630
|
+
if mid_src.is_dir():
|
|
631
|
+
self._copy_official_tree(mid_src, stage / "middlewares",
|
|
632
|
+
manifest_files, origin["package"])
|
|
625
633
|
# 2) utilities:官方构建资产;host 工具(gsar/eLink32pro)默认随包
|
|
626
634
|
if kernel == "m4":
|
|
627
635
|
board_asset = r.recipe.get("board_asset_dir") or board_dir
|
|
@@ -698,6 +706,15 @@ class Materializer:
|
|
|
698
706
|
deviations.append(dict(kind="example-doc-excluded", detail=sf["file_name"],
|
|
699
707
|
reason="官方示例文档,非构建资产"))
|
|
700
708
|
continue
|
|
709
|
+
# 工程自有资产(system 时钟/看门狗配置、startup 栈配置)必须落在 project/ 内。
|
|
710
|
+
# 写进 library/ 会在用户整体更新库时被官方原档覆盖,工程配置静默失效。
|
|
711
|
+
if sf["kind"] in ("cmsis", "mdk") and \
|
|
712
|
+
sf["target_path"].replace("/", "\\").startswith("..\\..\\library\\"):
|
|
713
|
+
raise ContractError(
|
|
714
|
+
RECIPE_MISMATCH,
|
|
715
|
+
f"{cfg['config_id']}: 工程自有文件 {sf['file_name']} 指向 library/"
|
|
716
|
+
f"({sf['target_path']});system/startup 必须随工程放在 "
|
|
717
|
+
f"project/Src_USER 或 project/MDK_ARMv5")
|
|
701
718
|
target_file = _resolve_target(stage, sf["target_path"])
|
|
702
719
|
in_tree = target_file is None
|
|
703
720
|
base_name = Path(sf["origin"]).name
|
|
@@ -831,6 +848,28 @@ class Materializer:
|
|
|
831
848
|
_remap_includepath, text)
|
|
832
849
|
if n_inc:
|
|
833
850
|
edits.append(f"IncludePath remapped ({n_inc} nodes scanned)")
|
|
851
|
+
# b2) M4 官方 middlewares:官方板级模板本身不含 middlewares,官方逐 IC 生成工程
|
|
852
|
+
# 会追加 usb_drivers/inc;随工程携带 middlewares/ 后同步补进 IncludePath
|
|
853
|
+
# (F493x5 的目录名是 usbd_drivers)。
|
|
854
|
+
mw_incs: list[str] = []
|
|
855
|
+
if kernel == "m4":
|
|
856
|
+
mw_root = pkg / "middlewares"
|
|
857
|
+
for drv in ("usb_drivers", "usbd_drivers"):
|
|
858
|
+
if (mw_root / drv / "inc").is_dir():
|
|
859
|
+
mw_incs.append(f"..\\..\\middlewares\\{drv}\\inc")
|
|
860
|
+
if mw_incs:
|
|
861
|
+
def _add_middleware_incs(m):
|
|
862
|
+
body = m.group(2)
|
|
863
|
+
if not body.strip():
|
|
864
|
+
return m.group(0)
|
|
865
|
+
entries = body.split(";")
|
|
866
|
+
for inc in mw_incs:
|
|
867
|
+
if inc not in entries:
|
|
868
|
+
entries.append(inc)
|
|
869
|
+
edits.append(f"IncludePath += {inc} (official middlewares)")
|
|
870
|
+
return m.group(1) + ";".join(entries) + m.group(3)
|
|
871
|
+
text = re.sub(r"(<IncludePath>)([^<]*)(</IncludePath>)",
|
|
872
|
+
_add_middleware_incs, text)
|
|
834
873
|
# c) Device / Cpu / 内存节点(一处裁决多处一致)
|
|
835
874
|
mem = cfg["memory"]
|
|
836
875
|
flash_base = int(mem["flash"][0]["base"], 16)
|
|
@@ -851,10 +890,9 @@ class Materializer:
|
|
|
851
890
|
if f"<{tag}>" in text:
|
|
852
891
|
text = rewrite_memory_node(text, tag, f"0x{base:08X}", f"0x{size:X}", what)
|
|
853
892
|
edits.append("memory nodes (Cpu/IROM/IRAM/OCR_RVCT4/OCR_RVCT9) from recipe model")
|
|
854
|
-
# umfTarg
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
edits.append("umfTarg 0->1 (Use Memory Layout from Target Dialog)")
|
|
893
|
+
# umfTarg:与官方逐 IC 生成工程一致(0 = 不使用 Target Dialog 内存布局);
|
|
894
|
+
# M4 板级模板原值是 1,由 recipe 的 project_edits 声明式改为 0;
|
|
895
|
+
# M0+/M3 官方原值已是 0,这里不再强制改写。
|
|
858
896
|
# FlashDriverDll:-FL 与裁决容量一致(官方逐 IC 值优先;仅裁决覆盖时改写)
|
|
859
897
|
# 注意 UL2CM3 参数是**按算法块分块**的:
|
|
860
898
|
# UL2CM3(... -FF0HT32F -FS00 -FL<主 Flash 容量> -FF1HT32F_OPT -FS11FF00000 -FL<OPT 区长度>)
|
|
@@ -904,6 +942,28 @@ class Materializer:
|
|
|
904
942
|
# e2) 声明式 uvprojx 选项编辑(如 M4 统一 -O2;命中数必须精确)
|
|
905
943
|
for e in cfg.get("project_edits", []):
|
|
906
944
|
tag = e["tag"]
|
|
945
|
+
if e.get("op") == "ensure":
|
|
946
|
+
# 节点存在则改值,不存在则在指定锚点后插入(M0+/M3 的 uC99)
|
|
947
|
+
m0 = re.search(rf"<{tag}>([^<]*)</{tag}>", text)
|
|
948
|
+
old_val = m0.group(1) if m0 else None
|
|
949
|
+
if m0:
|
|
950
|
+
text = rewrite_tag_value(text, tag, e["new_value"], what)
|
|
951
|
+
edits.append(f"{tag} ensure -> {e['new_value']}")
|
|
952
|
+
else:
|
|
953
|
+
anchor = e["insert_after_tag"]
|
|
954
|
+
m = re.search(rf"([ \t]*)<{anchor}>[^<]*</{anchor}>(\r?\n)", text)
|
|
955
|
+
if not m:
|
|
956
|
+
raise ContractError(
|
|
957
|
+
RECIPE_MISMATCH,
|
|
958
|
+
f"{what}: <{tag}> 不存在且找不到插入锚点 <{anchor}>")
|
|
959
|
+
ins = f"{m.group(1)}<{tag}>{e['new_value']}</{tag}>{m.group(2)}"
|
|
960
|
+
text = text[:m.end()] + ins + text[m.end():]
|
|
961
|
+
edits.append(f"{tag} inserted -> {e['new_value']}")
|
|
962
|
+
deviations.append(dict(
|
|
963
|
+
kind="project-option-edit", file=f"{stem}.uvprojx", tag=tag,
|
|
964
|
+
old=old_val, new=e["new_value"],
|
|
965
|
+
reason=e.get("reason") or "声明式项目选项编辑"))
|
|
966
|
+
continue
|
|
907
967
|
old = f"<{tag}>{e['old_value']}</{tag}>"
|
|
908
968
|
new = f"<{tag}>{e['new_value']}</{tag}>"
|
|
909
969
|
hits = text.count(old)
|
|
@@ -1139,19 +1199,36 @@ class Materializer:
|
|
|
1139
1199
|
lines.append(" - printf 经 C 库 + 官方 retarget 钩子输出;波特率 115200")
|
|
1140
1200
|
else:
|
|
1141
1201
|
lines.append(" - 官方 `_RETARGET=0`:printf/getchar 退化为空操作,不占用 UART")
|
|
1142
|
-
|
|
1143
|
-
"",
|
|
1144
|
-
"## 目录",
|
|
1145
|
-
"",
|
|
1146
|
-
"| 目录 | 内容 |",
|
|
1147
|
-
"|---|---|",
|
|
1202
|
+
dir_rows = [
|
|
1148
1203
|
"| `library/` | 完整官方库(字节冻结,manifest/files.json 可逐文件核对) |",
|
|
1204
|
+
]
|
|
1205
|
+
if kernel == "m4":
|
|
1206
|
+
dir_rows.append(
|
|
1207
|
+
"| `middlewares/` | 官方中间件:USB drivers/classes、FatFS、"
|
|
1208
|
+
"I2C application library(官方式 IncludePath 引用 "
|
|
1209
|
+
"`middlewares\\usb_drivers\\inc`) |")
|
|
1210
|
+
dir_rows += [
|
|
1149
1211
|
"| `utilities/` | 官方板级/公共构建资产 + host 工具(`gsar/`、`elink32pro/`)"
|
|
1150
1212
|
"+ DFP 安装包(`pack/`,见上「前置条件」) |",
|
|
1151
|
-
"| `project/MDK_ARMv5/` |
|
|
1152
|
-
"
|
|
1213
|
+
"| `project/MDK_ARMv5/` | 工程文件(官方工程声明式物化)"
|
|
1214
|
+
" + 工程自有 `startup_*.s`(栈配置) |",
|
|
1215
|
+
"| `project/Src_USER/` | 用户源码:自有 `main.c` + 官方配置/中断文件"
|
|
1216
|
+
" + 工程自有 `system_*.c`(时钟/看门狗配置) |",
|
|
1153
1217
|
"| `licenses/` | 官方许可文件 |",
|
|
1154
1218
|
"| `manifest/` | project-lock / files / deviations / validation 四件套 |",
|
|
1219
|
+
]
|
|
1220
|
+
lines += ["", "## 目录", "", "| 目录 | 内容 |", "|---|---|", *dir_rows]
|
|
1221
|
+
lines += [
|
|
1222
|
+
"",
|
|
1223
|
+
"## 更新官方库",
|
|
1224
|
+
"",
|
|
1225
|
+
"`library/`、`utilities/`"
|
|
1226
|
+
+ ("、`middlewares/`" if kernel == "m4" else "")
|
|
1227
|
+
+ " 是官方资产,可整体替换为同版本或更新的官方包;"
|
|
1228
|
+
"工程自己的配置都在 `project/` 下(`system_*.c` 时钟/看门狗、"
|
|
1229
|
+
"`startup_*.s` 栈、`<series>_conf.h`、`main.c`),**不会被库更新覆盖**。"
|
|
1230
|
+
"替换库后如需合并官方改动,请只 diff `system_*.c` / `startup_*.s`,"
|
|
1231
|
+
"不要直接用官方原档覆盖工程自有副本。",
|
|
1155
1232
|
"",
|
|
1156
1233
|
"## 自校验",
|
|
1157
1234
|
"",
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""回归测试:导出侧实测暴露的三个缺陷。
|
|
2
|
+
|
|
3
|
+
1) 云端 zip 模式下响应格式化用了未定义的 `_fmt_time` → 线上 `export_project_bundle`
|
|
4
|
+
100% 失败(本地模式与直接调 `exporter.export_zip` 都覆盖不到这行)。
|
|
5
|
+
2) 物化器的单次调用状态(`_stage` / `_project_rel`)曾挂在进程级单例实例上,
|
|
6
|
+
并发导出互相覆盖 → "…is not in the subpath of…" 串台错误。
|
|
7
|
+
3) 默认时钟 profile `hsi-safe` 曾整体失效:官方 system 文件只存在于 `library/`,由完整
|
|
8
|
+
官方树按字节拷贝,而选项编辑只作用于 `_resolve_target` 能定位到 staging 的文件 →
|
|
9
|
+
声明式编辑从未落到 system 文件,导出工程仍依赖外部晶振(HSE)。现在的口径是:
|
|
10
|
+
system/startup 属于**工程自有资产**,导出到 `project/Src_USER` 与
|
|
11
|
+
`project/MDK_ARMv5`;`library/` 保持官方原档字节,用户整体更新库不会覆盖工程配置。
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import concurrent.futures
|
|
16
|
+
import json
|
|
17
|
+
import re
|
|
18
|
+
|
|
19
|
+
from ht32_code_mcp import server as srv
|
|
20
|
+
|
|
21
|
+
CFG_F52352 = "ht32f52352.ht32f52352-sk.ac5.hsi-safe.retarget"
|
|
22
|
+
CFG_F50241 = "ht32f50241.ht32f50241-sk.ac5.hsi-safe.retarget"
|
|
23
|
+
CFG_L52241 = "ht32l52241.ht32l52241-sk.ac5.hsi-safe.retarget"
|
|
24
|
+
CFG_F49163 = "ht32f49163-100lqfp.ht32f49163-sk.ac5.hick-safe.retarget"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_cloud_zip_response_formats_expiry(tmp_path, monkeypatch):
|
|
28
|
+
"""云端模式必须能给出下载 URL 与有效期(不得因未定义的 helper 直接抛 NameError)。"""
|
|
29
|
+
monkeypatch.setattr(srv, "CLOUD_BUNDLE", True)
|
|
30
|
+
monkeypatch.setattr(srv, "BUNDLE_DIR", str(tmp_path / "bundles"))
|
|
31
|
+
monkeypatch.setattr(srv, "BUNDLE_BASE_URL", "http://example.test/ht32-bundles")
|
|
32
|
+
text = srv.export_project_bundle(config_id=CFG_F52352, project_name="CloudMode")
|
|
33
|
+
assert "导出失败" not in text, text
|
|
34
|
+
assert "下载 URL" in text and "http://example.test/ht32-bundles/" in text
|
|
35
|
+
assert "有效期" in text
|
|
36
|
+
assert list(tmp_path.joinpath("bundles").glob("*.zip")), "未生成 zip 产物"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_concurrent_exports_do_not_cross_talk(tmp_path):
|
|
40
|
+
"""并发导出必须各自落到自己的目录(不得因共享实例状态串台)。"""
|
|
41
|
+
jobs = [(CFG_F50241, "ConcA"), (CFG_L52241, "ConcB")]
|
|
42
|
+
|
|
43
|
+
def run(item):
|
|
44
|
+
idx, (cid, name) = item
|
|
45
|
+
return srv.export_project_bundle(
|
|
46
|
+
config_id=cid, project_name=name, out_dir=str(tmp_path / f"out{idx}"))
|
|
47
|
+
|
|
48
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as pool:
|
|
49
|
+
texts = list(pool.map(run, enumerate(jobs)))
|
|
50
|
+
|
|
51
|
+
for text, (_, name) in zip(texts, jobs):
|
|
52
|
+
assert "导出失败" not in text, text
|
|
53
|
+
for idx, (_, name) in enumerate(jobs):
|
|
54
|
+
proj = tmp_path / f"out{idx}" / "project" / "MDK_ARMv5" / f"{name}.uvprojx"
|
|
55
|
+
assert proj.is_file(), f"并发导出产物缺失:{proj}"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_hsi_safe_profile_edits_project_owned_system_file(tmp_path):
|
|
59
|
+
"""hsi-safe 改写工程自有 system 副本,library/ 必须保持官方字节。
|
|
60
|
+
|
|
61
|
+
F50241 的官方 system 文件属 hse-direct 家族(HSE_ENABLE=1 / HCLK_SRC=2),
|
|
62
|
+
导出后 `project/Src_USER` 下的副本应改为 HSE_ENABLE=0 + HCLK_SRC=3(HSI,
|
|
63
|
+
20 MHz 满频),并在 deviations 留 option-edit 记录;`library/` 下的同名
|
|
64
|
+
官方文件保持原样,这样用户整体更新库时不会把工程配置清掉。
|
|
65
|
+
"""
|
|
66
|
+
out = tmp_path / "out"
|
|
67
|
+
text = srv.export_project_bundle(config_id=CFG_F50241, project_name="HsiSafe",
|
|
68
|
+
out_dir=str(out))
|
|
69
|
+
assert "导出失败" not in text, text
|
|
70
|
+
|
|
71
|
+
dev = json.loads((out / "manifest" / "deviations.json").read_text(encoding="utf-8"))
|
|
72
|
+
edits = [d for d in dev["deviations"] if d["kind"] == "option-edit"]
|
|
73
|
+
assert {d["selector"] for d in edits} >= {"HSE_ENABLE", "HCLK_SRC"}, f"缺 option-edit 记录:{edits}"
|
|
74
|
+
assert len({d["file"] for d in edits}) == 1, f"option-edit 跨了多个文件:{edits}"
|
|
75
|
+
sys_name = edits[0]["file"]
|
|
76
|
+
|
|
77
|
+
owned = out / "project" / "Src_USER" / sys_name
|
|
78
|
+
assert owned.is_file(), f"工程自有副本缺失:{owned}"
|
|
79
|
+
code = owned.read_text(encoding="utf-8", errors="replace")
|
|
80
|
+
assert re.search(r"#define\s+HSE_ENABLE\s+\(0\)", code), "HSE_ENABLE 未被改写为 0"
|
|
81
|
+
assert re.search(r"#define\s+HCLK_SRC\s+\(3\)", code), "HCLK_SRC 未被改写为 3(HSI)"
|
|
82
|
+
|
|
83
|
+
library_copy = next((p for p in out.glob("library/Device/**/system_*.c")
|
|
84
|
+
if p.name == sys_name), None)
|
|
85
|
+
assert library_copy is not None, f"完整官方库缺少 {sys_name}"
|
|
86
|
+
lib_code = library_copy.read_text(encoding="utf-8", errors="replace")
|
|
87
|
+
assert re.search(r"#define\s+HSE_ENABLE\s+\(1\)", lib_code), \
|
|
88
|
+
"library/ 下的官方 system 文件被改写(更新库会覆盖工程配置)"
|
|
89
|
+
assert re.search(r"#define\s+HCLK_SRC\s+\(2\)", lib_code), \
|
|
90
|
+
"library/ 下的官方 system 文件被改写(更新库会覆盖工程配置)"
|
|
91
|
+
|
|
92
|
+
files = json.loads((out / "manifest" / "files.json").read_text(encoding="utf-8"))
|
|
93
|
+
paths = [f["path"] for f in files["files"]]
|
|
94
|
+
assert len(paths) == len(set(paths)), "同一路径被重复登记(树拷贝 + 源集各一次)"
|
|
95
|
+
owned_entry = next(f for f in files["files"]
|
|
96
|
+
if f["path"] == f"project/Src_USER/{sys_name}")
|
|
97
|
+
assert owned_entry.get("edited") is True, "工程自有 system 副本未标记 edited"
|
|
98
|
+
lib_entry = next(f for f in files["files"]
|
|
99
|
+
if f["path"].startswith("library/") and f["path"].endswith("/" + sys_name))
|
|
100
|
+
assert not lib_entry.get("edited"), "library/ 官方文件不应被标记 edited"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_m4_system_startup_are_project_owned(tmp_path):
|
|
104
|
+
"""M4 与 M0+/M3 同口径:system/startup 放 project/,library 保持官方字节。
|
|
105
|
+
|
|
106
|
+
官方 M4 生成工程把这两个文件放在 libraries/cmsis 下;若导出仍引用库内副本,
|
|
107
|
+
用户整体更新 library/ 会覆盖工程配置(与 M0+/M3 修复前同一个问题)。
|
|
108
|
+
"""
|
|
109
|
+
out = tmp_path / "m4"
|
|
110
|
+
text = srv.export_project_bundle(config_id=CFG_F49163, project_name="M4Owned",
|
|
111
|
+
out_dir=str(out))
|
|
112
|
+
assert "导出失败" not in text, text
|
|
113
|
+
|
|
114
|
+
sys_name = "system_ht32f491x3.c"
|
|
115
|
+
startup_name = "startup_ht32f491x3.s"
|
|
116
|
+
owned_sys = out / "project" / "Src_USER" / sys_name
|
|
117
|
+
owned_startup = out / "project" / "MDK_ARMv5" / startup_name
|
|
118
|
+
assert owned_sys.is_file(), f"缺工程自有 system 副本:{owned_sys}"
|
|
119
|
+
assert owned_startup.is_file(), f"缺工程自有 startup 副本:{owned_startup}"
|
|
120
|
+
|
|
121
|
+
# 完整官方库仍保留同名文件(作为官方资产),但构建引用的是 project/ 副本
|
|
122
|
+
assert next(out.glob(f"library/**/{sys_name}"), None) is not None
|
|
123
|
+
assert next(out.glob(f"library/**/{startup_name}"), None) is not None
|
|
124
|
+
uv = (out / "project" / "MDK_ARMv5" / "M4Owned.uvprojx").read_text(
|
|
125
|
+
encoding="utf-8", errors="replace")
|
|
126
|
+
assert f"..\\Src_USER\\{sys_name}" in uv, "uvprojx 未引用工程自有 system 副本"
|
|
127
|
+
assert f".\\{startup_name}" in uv, "uvprojx 未引用工程自有 startup 副本"
|
|
128
|
+
|
|
129
|
+
files = json.loads((out / "manifest" / "files.json").read_text(encoding="utf-8"))
|
|
130
|
+
paths = [f["path"] for f in files["files"]]
|
|
131
|
+
assert len(paths) == len(set(paths)), "同一路径被重复登记"
|
|
132
|
+
entry = next(f for f in files["files"] if f["path"] == f"project/Src_USER/{sys_name}")
|
|
133
|
+
assert not entry.get("edited"), "M4 默认 profile 不应改写 system 文件"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_m4_export_carries_official_middlewares(tmp_path):
|
|
137
|
+
"""M4 官方包自带 middlewares/(USB/FatFS/I2C),导出必须携带、登记并补 IncludePath。"""
|
|
138
|
+
out = tmp_path / "m4mw"
|
|
139
|
+
text = srv.export_project_bundle(config_id=CFG_F49163, project_name="M4Mid",
|
|
140
|
+
out_dir=str(out))
|
|
141
|
+
assert "导出失败" not in text, text
|
|
142
|
+
|
|
143
|
+
usb_inc = out / "middlewares" / "usb_drivers" / "inc" / "usbd_core.h"
|
|
144
|
+
assert usb_inc.is_file(), "缺少 middlewares/usb_drivers/inc(官方 M4 包自带)"
|
|
145
|
+
|
|
146
|
+
files = json.loads((out / "manifest" / "files.json").read_text(encoding="utf-8"))
|
|
147
|
+
paths = {f["path"] for f in files["files"]}
|
|
148
|
+
assert "middlewares/usb_drivers/inc/usbd_core.h" in paths, \
|
|
149
|
+
"middlewares 未登记在 manifest/files.json"
|
|
150
|
+
|
|
151
|
+
uv = (out / "project" / "MDK_ARMv5" / "M4Mid.uvprojx").read_text(encoding="utf-8")
|
|
152
|
+
assert "..\\..\\middlewares\\usb_drivers\\inc" in uv, \
|
|
153
|
+
"IncludePath 未指向 middlewares/usb_drivers/inc"
|
|
154
|
+
assert "<uC99>1</uC99>" in uv, "M4 未启用 C99(应与官方生成工程对齐)"
|
|
155
|
+
assert "<Misc>--entry Reset_Handler</Misc>" in uv, \
|
|
156
|
+
"M4 链接入口未与官方生成工程对齐"
|
|
157
|
+
assert "<umfTarg>0</umfTarg>" in uv, "M4 umfTarg 应与官方生成工程一致(0)"
|
|
158
|
+
|
|
159
|
+
# M0+ 官方包没有 middlewares,不应凭空出现
|
|
160
|
+
out2 = tmp_path / "m0nomid"
|
|
161
|
+
srv.export_project_bundle(config_id=CFG_F52352, project_name="M0NoMid",
|
|
162
|
+
out_dir=str(out2))
|
|
163
|
+
assert not (out2 / "middlewares").exists(), "M0+ 工程不应携带 middlewares"
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def test_c99_enabled_for_all_series(tmp_path):
|
|
167
|
+
"""全系列统一开启 C99;M0+/M3 由 ensure 编辑插入 uC99 节点。"""
|
|
168
|
+
cases = [(CFG_F52352, "F5C99"), (CFG_L52241, "L5C99"), (CFG_F49163, "M4C99")]
|
|
169
|
+
for cfg, name in cases:
|
|
170
|
+
out = tmp_path / name
|
|
171
|
+
text = srv.export_project_bundle(config_id=cfg, project_name=name,
|
|
172
|
+
out_dir=str(out))
|
|
173
|
+
assert "导出失败" not in text, text
|
|
174
|
+
uv = next((out / "project" / "MDK_ARMv5").glob("*.uvprojx"))
|
|
175
|
+
xml = uv.read_text(encoding="utf-8")
|
|
176
|
+
assert "<uC99>1</uC99>" in xml, f"{cfg} 未开启 C99"
|
|
@@ -273,7 +273,7 @@ def _bundle_checks_v2(out_dir: Path, device: str, config_id: str = ""):
|
|
|
273
273
|
assert m and int(m.group(1), 16) == flash_size, "IROM 与锁定 memory 不一致"
|
|
274
274
|
m = re.search(r"<IRAM>.*?<Size>(0x[0-9A-Fa-f]+)</Size>", text, re.S)
|
|
275
275
|
assert m and int(m.group(1), 16) == ram_size, "IRAM 与锁定 memory 不一致"
|
|
276
|
-
assert "<umfTarg>
|
|
276
|
+
assert "<umfTarg>0</umfTarg>" in text, "umfTarg 应与官方生成工程一致(0)"
|
|
277
277
|
# 模板自用文件不进用户工程;README 属于用户工程
|
|
278
278
|
assert not (out_dir / "devices.json").exists()
|
|
279
279
|
assert not (out_dir / "TEMPLATE.md").exists()
|
|
@@ -381,7 +381,9 @@ def test_export_positive_default_chip(tmp_path):
|
|
|
381
381
|
out = tmp_path / "bundle_default"
|
|
382
382
|
result = srv.export_project_bundle("HT32F52352", str(out))
|
|
383
383
|
assert "已导出" in result and "contract_version: 2" in result
|
|
384
|
-
|
|
384
|
+
rev = json.loads((REPO / ".work/template-recipes/recipes/ht32f5.json")
|
|
385
|
+
.read_text(encoding="utf-8"))["recipe_revision"]
|
|
386
|
+
assert "config_id: ht32f52352." in result and f"recipe_revision: {rev}" in result
|
|
385
387
|
assert "v1 chip 参数已弃用" in result # 旧参数走只读适配层
|
|
386
388
|
assert "flash" not in result.lower() or "Flash" in result
|
|
387
389
|
_bundle_checks_v2(out, "HT32F52352")
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
"""回归测试:导出侧实测暴露的三个缺陷。
|
|
2
|
-
|
|
3
|
-
1) 云端 zip 模式下响应格式化用了未定义的 `_fmt_time` → 线上 `export_project_bundle`
|
|
4
|
-
100% 失败(本地模式与直接调 `exporter.export_zip` 都覆盖不到这行)。
|
|
5
|
-
2) 物化器的单次调用状态(`_stage` / `_project_rel`)曾挂在进程级单例实例上,
|
|
6
|
-
并发导出互相覆盖 → "…is not in the subpath of…" 串台错误。
|
|
7
|
-
3) 默认时钟 profile `hsi-safe` 曾整体失效:官方 system 文件在 `library/` 下,由完整官方树
|
|
8
|
-
按字节拷贝,而选项编辑只作用于 `_resolve_target` 能定位到 staging 的文件 → 声明式编辑
|
|
9
|
-
从未落到 system 文件,导出工程仍依赖外部晶振(HSE)。
|
|
10
|
-
"""
|
|
11
|
-
from __future__ import annotations
|
|
12
|
-
|
|
13
|
-
import concurrent.futures
|
|
14
|
-
import json
|
|
15
|
-
import re
|
|
16
|
-
|
|
17
|
-
from ht32_code_mcp import server as srv
|
|
18
|
-
|
|
19
|
-
CFG_F52352 = "ht32f52352.ht32f52352-sk.ac5.hsi-safe.retarget"
|
|
20
|
-
CFG_F50241 = "ht32f50241.ht32f50241-sk.ac5.hsi-safe.retarget"
|
|
21
|
-
CFG_L52241 = "ht32l52241.ht32l52241-sk.ac5.hsi-safe.retarget"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def test_cloud_zip_response_formats_expiry(tmp_path, monkeypatch):
|
|
25
|
-
"""云端模式必须能给出下载 URL 与有效期(不得因未定义的 helper 直接抛 NameError)。"""
|
|
26
|
-
monkeypatch.setattr(srv, "CLOUD_BUNDLE", True)
|
|
27
|
-
monkeypatch.setattr(srv, "BUNDLE_DIR", str(tmp_path / "bundles"))
|
|
28
|
-
monkeypatch.setattr(srv, "BUNDLE_BASE_URL", "http://example.test/ht32-bundles")
|
|
29
|
-
text = srv.export_project_bundle(config_id=CFG_F52352, project_name="CloudMode")
|
|
30
|
-
assert "导出失败" not in text, text
|
|
31
|
-
assert "下载 URL" in text and "http://example.test/ht32-bundles/" in text
|
|
32
|
-
assert "有效期" in text
|
|
33
|
-
assert list(tmp_path.joinpath("bundles").glob("*.zip")), "未生成 zip 产物"
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def test_concurrent_exports_do_not_cross_talk(tmp_path):
|
|
37
|
-
"""并发导出必须各自落到自己的目录(不得因共享实例状态串台)。"""
|
|
38
|
-
jobs = [(CFG_F50241, "ConcA"), (CFG_L52241, "ConcB")]
|
|
39
|
-
|
|
40
|
-
def run(item):
|
|
41
|
-
idx, (cid, name) = item
|
|
42
|
-
return srv.export_project_bundle(
|
|
43
|
-
config_id=cid, project_name=name, out_dir=str(tmp_path / f"out{idx}"))
|
|
44
|
-
|
|
45
|
-
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as pool:
|
|
46
|
-
texts = list(pool.map(run, enumerate(jobs)))
|
|
47
|
-
|
|
48
|
-
for text, (_, name) in zip(texts, jobs):
|
|
49
|
-
assert "导出失败" not in text, text
|
|
50
|
-
for idx, (_, name) in enumerate(jobs):
|
|
51
|
-
proj = tmp_path / f"out{idx}" / "project" / "MDK_ARMv5" / f"{name}.uvprojx"
|
|
52
|
-
assert proj.is_file(), f"并发导出产物缺失:{proj}"
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def test_hsi_safe_profile_edits_system_file_in_library_tree(tmp_path):
|
|
56
|
-
"""hsi-safe 默认 profile 必须真正改写官方 system 文件(不得因 tree 拷贝路径整条跳过)。
|
|
57
|
-
|
|
58
|
-
F50241 的官方 system 文件属 hse-direct 家族(HCLK_SRC=2/HSE),编辑器应把它改成
|
|
59
|
-
HSE_ENABLE=0 + HCLK_SRC=3(HSI,20 MHz 满频),并在 deviations 留 option-edit 记录。
|
|
60
|
-
"""
|
|
61
|
-
out = tmp_path / "out"
|
|
62
|
-
text = srv.export_project_bundle(config_id=CFG_F50241, project_name="HsiSafe",
|
|
63
|
-
out_dir=str(out))
|
|
64
|
-
assert "导出失败" not in text, text
|
|
65
|
-
|
|
66
|
-
# 完整官方树里有 ~20 个 system_*.c;只有本配置选中的那个应被改写
|
|
67
|
-
dev = json.loads((out / "manifest" / "deviations.json").read_text(encoding="utf-8"))
|
|
68
|
-
edits = [d for d in dev["deviations"] if d["kind"] == "option-edit"]
|
|
69
|
-
assert {d["selector"] for d in edits} >= {"HSE_ENABLE", "HCLK_SRC"}, f"缺 option-edit 记录:{edits}"
|
|
70
|
-
assert len({d["file"] for d in edits}) == 1, f"option-edit 跨了多个文件:{edits}"
|
|
71
|
-
sys_name = edits[0]["file"]
|
|
72
|
-
|
|
73
|
-
target = next((p for p in out.glob("library/Device/**/system_*.c") if p.name == sys_name), None)
|
|
74
|
-
assert target is not None, f"导出物里找不到 {sys_name}"
|
|
75
|
-
code = target.read_text(encoding="utf-8", errors="replace")
|
|
76
|
-
assert re.search(r"#define\s+HSE_ENABLE\s+\(0\)", code), "HSE_ENABLE 未被改写为 0"
|
|
77
|
-
assert re.search(r"#define\s+HCLK_SRC\s+\(3\)", code), "HCLK_SRC 未被改写为 3(HSI)"
|
|
78
|
-
|
|
79
|
-
files = json.loads((out / "manifest" / "files.json").read_text(encoding="utf-8"))
|
|
80
|
-
paths = [f["path"] for f in files["files"]]
|
|
81
|
-
assert len(paths) == len(set(paths)), "同一路径被重复登记(树拷贝 + 源集各一次)"
|
|
82
|
-
entry = next(f for f in files["files"] if f["path"].endswith("/" + sys_name))
|
|
83
|
-
assert entry.get("edited") is True, "system 文件的 manifest 条目未标记 edited"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|