mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-07 00:39:00 +08:00
fix(tooling): 补全PR测试报告解析并修复并发测试
- 更新 gframework-pr-review 脚本以提取 CTRF 测试摘要和失败用例详情 - 修复 SettingsModelTests 在 NET9+ 下错误使用 Monitor 持锁的并发测试语义 - 同步 analyzer-warning-reduction 的 active todo 与 trace 真值
This commit is contained in:
parent
1f560635a8
commit
1c87272f6b
@ -62,6 +62,7 @@ The script should produce:
|
|||||||
- Pre-merge failed checks, if present
|
- Pre-merge failed checks, if present
|
||||||
- Latest MegaLinter status and any detailed issues posted by `github-actions[bot]`
|
- Latest MegaLinter status and any detailed issues posted by `github-actions[bot]`
|
||||||
- Test summary, including failed-test signals when present
|
- Test summary, including failed-test signals when present
|
||||||
|
- Detailed failed-test rows from GitHub Test Reporter / CTRF comments when the PR comment includes `Name` / `Failure Message` content
|
||||||
- CLI support for writing full JSON to a file and printing only narrowed text sections to stdout
|
- CLI support for writing full JSON to a file and printing only narrowed text sections to stdout
|
||||||
- Parse warnings only when both the primary API source and the intended fallback signal are unavailable
|
- Parse warnings only when both the primary API source and the intended fallback signal are unavailable
|
||||||
|
|
||||||
|
|||||||
@ -257,6 +257,11 @@ def strip_markdown_links(text: str) -> str:
|
|||||||
return re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", text)
|
return re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", text)
|
||||||
|
|
||||||
|
|
||||||
|
def strip_markdown_images(text: str) -> str:
|
||||||
|
"""Drop Markdown image syntax while keeping surrounding text readable."""
|
||||||
|
return re.sub(r"!\[[^\]]*\]\([^)]+\)", "", text)
|
||||||
|
|
||||||
|
|
||||||
def extract_section(text: str, start_marker: str, end_markers: list[str]) -> str | None:
|
def extract_section(text: str, start_marker: str, end_markers: list[str]) -> str | None:
|
||||||
"""Extract text between a start marker and the earliest matching end marker."""
|
"""Extract text between a start marker and the earliest matching end marker."""
|
||||||
start = text.find(start_marker)
|
start = text.find(start_marker)
|
||||||
@ -486,43 +491,190 @@ def parse_megalinter_comment(comment_body: str) -> dict[str, Any]:
|
|||||||
return report
|
return report
|
||||||
|
|
||||||
|
|
||||||
|
def clean_markdown_table_cell(text: str) -> str:
|
||||||
|
"""Normalize a Markdown table cell for structured parsing."""
|
||||||
|
cleaned = strip_markdown_images(strip_markdown_links(html.unescape(text)))
|
||||||
|
cleaned = cleaned.replace("\xa0", " ")
|
||||||
|
cleaned = cleaned.replace("**", "").replace("*", "").replace("`", "")
|
||||||
|
return collapse_whitespace(cleaned)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_int_from_text(text: str) -> int | None:
|
||||||
|
"""Extract the first integer value from text."""
|
||||||
|
match = re.search(r"\d+", text)
|
||||||
|
return int(match.group(0)) if match else None
|
||||||
|
|
||||||
|
|
||||||
|
def parse_duration_from_text(text: str) -> str:
|
||||||
|
"""Extract a duration token from text when present."""
|
||||||
|
match = re.search(r"\d+(?:\.\d+)?(?:ms|s|m|h)", text)
|
||||||
|
if match is not None:
|
||||||
|
return match.group(0)
|
||||||
|
|
||||||
|
return collapse_whitespace(text)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_markdown_table(table_text: str) -> tuple[list[str], list[list[str]]]:
|
||||||
|
"""Parse a Markdown table into header cells and row cells."""
|
||||||
|
lines = [line.strip() for line in table_text.splitlines() if line.strip().startswith("|")]
|
||||||
|
if len(lines) < 2:
|
||||||
|
return [], []
|
||||||
|
|
||||||
|
headers = [clean_markdown_table_cell(cell) for cell in lines[0].strip("|").split("|")]
|
||||||
|
rows: list[list[str]] = []
|
||||||
|
for line in lines[2:]:
|
||||||
|
cells = [clean_markdown_table_cell(cell) for cell in line.strip("|").split("|")]
|
||||||
|
if cells:
|
||||||
|
rows.append(cells)
|
||||||
|
|
||||||
|
return headers, rows
|
||||||
|
|
||||||
|
|
||||||
|
def extract_markdown_table_after_heading(block: str, heading: str) -> tuple[list[str], list[list[str]]]:
|
||||||
|
"""Extract the first Markdown table that appears after a heading."""
|
||||||
|
section = extract_section(block, heading, ["\n### ", "\n#### ", "\n<details>", "\n<table>", "\n<sub>"])
|
||||||
|
if section is None:
|
||||||
|
return [], []
|
||||||
|
|
||||||
|
table_match = re.search(r"(\|.*\|\n\|[-| :]+\|\n(?:\|.*\|\n?)*)", section, re.S)
|
||||||
|
if table_match is None:
|
||||||
|
return [], []
|
||||||
|
|
||||||
|
return parse_markdown_table(table_match.group(1))
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_stat_header(header: str) -> str:
|
||||||
|
"""Normalize a human-readable stats header into a stable machine key."""
|
||||||
|
ascii_only = re.sub(r"[^A-Za-z]+", "", header).lower()
|
||||||
|
aliases = {
|
||||||
|
"tests": "tests",
|
||||||
|
"passed": "passed",
|
||||||
|
"failed": "failed",
|
||||||
|
"skipped": "skipped",
|
||||||
|
"pending": "pending",
|
||||||
|
"other": "other",
|
||||||
|
"flaky": "flaky",
|
||||||
|
"duration": "duration",
|
||||||
|
}
|
||||||
|
return aliases.get(ascii_only, ascii_only)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_stats_table(headers: list[str], rows: list[list[str]]) -> dict[str, Any]:
|
||||||
|
"""Convert a parsed Markdown stats table into the report stats shape."""
|
||||||
|
if not headers or not rows:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
first_row = rows[0]
|
||||||
|
stats: dict[str, Any] = {}
|
||||||
|
for header, value in zip(headers, first_row):
|
||||||
|
key = normalize_stat_header(header)
|
||||||
|
if not key:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if key == "duration":
|
||||||
|
stats[key] = parse_duration_from_text(value)
|
||||||
|
continue
|
||||||
|
|
||||||
|
parsed_value = parse_int_from_text(value)
|
||||||
|
if parsed_value is not None:
|
||||||
|
stats[key] = parsed_value
|
||||||
|
|
||||||
|
return stats
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_failure_message(text: str) -> str:
|
||||||
|
"""Normalize a failed-test message while preserving the meaningful lines."""
|
||||||
|
cleaned = html.unescape(text)
|
||||||
|
cleaned = re.sub(r"(?i)<br\s*/?>", "\n", cleaned)
|
||||||
|
cleaned = re.sub(r"</?(?:p|div|tbody|thead|tr|td|th|table)>", "\n", cleaned)
|
||||||
|
cleaned = re.sub(r"<[^>]+>", " ", cleaned)
|
||||||
|
lines = [collapse_whitespace(line) for line in cleaned.splitlines()]
|
||||||
|
meaningful_lines = [line for line in lines if line]
|
||||||
|
return "\n".join(meaningful_lines)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_failed_test_summary_list(block: str) -> list[str]:
|
||||||
|
"""Parse the compact failed-tests summary list from CTRF details blocks."""
|
||||||
|
failed_tests_section = re.search(
|
||||||
|
r"<details><summary><strong>\s*Failed Tests.*?</summary>(?P<body>.*?)</details>",
|
||||||
|
block,
|
||||||
|
re.S,
|
||||||
|
)
|
||||||
|
if failed_tests_section is None:
|
||||||
|
return []
|
||||||
|
|
||||||
|
summary_body = strip_markdown_links(strip_markdown_images(html.unescape(failed_tests_section.group("body"))))
|
||||||
|
failed_tests: list[str] = []
|
||||||
|
for raw_line in summary_body.splitlines():
|
||||||
|
line = collapse_whitespace(raw_line)
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if "arrow-right" in raw_line:
|
||||||
|
parts = [part.strip() for part in line.split("arrow-right") if part.strip()]
|
||||||
|
candidate = parts[-1] if parts else line
|
||||||
|
elif ">" in line:
|
||||||
|
candidate = line.split(">")[-1].strip()
|
||||||
|
else:
|
||||||
|
candidate = line
|
||||||
|
|
||||||
|
if candidate:
|
||||||
|
failed_tests.append(candidate)
|
||||||
|
|
||||||
|
return failed_tests
|
||||||
|
|
||||||
|
|
||||||
|
def parse_failed_test_details(block: str) -> list[dict[str, str]]:
|
||||||
|
"""Parse the detailed failed-test HTML table from GitHub Test Reporter comments."""
|
||||||
|
details: list[dict[str, str]] = []
|
||||||
|
table_section = re.search(
|
||||||
|
r"### ❌ \*\*Some tests failed!\*\*.*?<tbody>(?P<body>.*?)</tbody>",
|
||||||
|
block,
|
||||||
|
re.S,
|
||||||
|
)
|
||||||
|
if table_section is None:
|
||||||
|
return details
|
||||||
|
|
||||||
|
for name_cell, message_cell in re.findall(r"<tr>\s*<td>(.*?)</td>\s*<td>(.*?)</td>\s*</tr>", table_section.group("body"), re.S):
|
||||||
|
name = collapse_whitespace(strip_tags(html.unescape(name_cell))).lstrip("❌").strip()
|
||||||
|
failure_message = normalize_failure_message(message_cell)
|
||||||
|
if name:
|
||||||
|
details.append(
|
||||||
|
{
|
||||||
|
"name": name,
|
||||||
|
"failure_message": failure_message,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return details
|
||||||
|
|
||||||
|
|
||||||
def parse_test_report(block: str) -> dict[str, Any]:
|
def parse_test_report(block: str) -> dict[str, Any]:
|
||||||
"""Parse a CTRF or GitHub test-reporter comment block."""
|
"""Parse a CTRF or GitHub test-reporter comment block."""
|
||||||
report: dict[str, Any] = {
|
report: dict[str, Any] = {
|
||||||
"raw": block.strip(),
|
"raw": block.strip(),
|
||||||
"stats": {},
|
"stats": {},
|
||||||
"failed_tests": [],
|
"failed_tests": [],
|
||||||
|
"failed_test_details": [],
|
||||||
"has_failed_tests": False,
|
"has_failed_tests": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
summary_row_match = re.search(
|
summary_headers, summary_rows = extract_markdown_table_after_heading(block, "### Summary")
|
||||||
r"\|\s*\*?\*?(\d+)\*?\*?\s*\|\s*\*?\*?(\d+)\*?\*?\s*\|\s*\*?\*?(\d+)\*?\*?\s*\|"
|
report["stats"] = parse_stats_table(summary_headers, summary_rows)
|
||||||
r"\s*\*?\*?(\d+)\*?\*?\s*\|\s*\*?\*?(\d+)\*?\*?\s*\|\s*\*?\*?(\d+)\*?\*?\s*\|\s*\*?\*?([^\|]+?)\*?\*?\s*\|",
|
|
||||||
block,
|
|
||||||
)
|
|
||||||
if summary_row_match is not None:
|
|
||||||
report["stats"] = {
|
|
||||||
"tests": int(summary_row_match.group(1)),
|
|
||||||
"passed": int(summary_row_match.group(2)),
|
|
||||||
"failed": int(summary_row_match.group(3)),
|
|
||||||
"skipped": int(summary_row_match.group(4)),
|
|
||||||
"other": int(summary_row_match.group(5)),
|
|
||||||
"flaky": int(summary_row_match.group(6)),
|
|
||||||
"duration": summary_row_match.group(7).strip(),
|
|
||||||
}
|
|
||||||
|
|
||||||
failed_tests_section = extract_section(
|
if not report["stats"]:
|
||||||
block,
|
build_headers, build_rows = extract_markdown_table_after_heading(block, "### build-and-test:")
|
||||||
"### Failed Tests",
|
report["stats"] = parse_stats_table(build_headers, build_rows)
|
||||||
["### Slowest Tests", "### Insights", "<sub>", "[Github Test Reporter]"],
|
|
||||||
)
|
failed_test_details = parse_failed_test_details(block)
|
||||||
if failed_tests_section:
|
failed_test_names = parse_failed_test_summary_list(block)
|
||||||
lines = [line.strip("- ").strip() for line in failed_tests_section.splitlines()[1:] if line.strip()]
|
if not failed_test_names and failed_test_details:
|
||||||
report["failed_tests"] = lines
|
failed_test_names = [detail["name"] for detail in failed_test_details]
|
||||||
report["has_failed_tests"] = True
|
|
||||||
elif "No failed tests in this run." in block or "All tests passed!" in block:
|
report["failed_tests"] = failed_test_names
|
||||||
report["failed_tests"] = []
|
report["failed_test_details"] = failed_test_details
|
||||||
report["has_failed_tests"] = False
|
failed_count = int(report["stats"].get("failed", 0) or 0)
|
||||||
|
report["has_failed_tests"] = bool(failed_test_names or failed_test_details or failed_count > 0)
|
||||||
|
|
||||||
return report
|
return report
|
||||||
|
|
||||||
@ -1103,8 +1255,17 @@ def format_text(
|
|||||||
lines.append(f"- Report {index}: no structured test stats parsed")
|
lines.append(f"- Report {index}: no structured test stats parsed")
|
||||||
|
|
||||||
if report["has_failed_tests"]:
|
if report["has_failed_tests"]:
|
||||||
for failed_test in report["failed_tests"]:
|
failed_test_details = report.get("failed_test_details", [])
|
||||||
lines.append(f" Failed test: {truncate_text(failed_test, max_description_length)}")
|
if failed_test_details:
|
||||||
|
for failed_test_detail in failed_test_details:
|
||||||
|
lines.append(f" Failed test: {truncate_text(failed_test_detail['name'], max_description_length)}")
|
||||||
|
lines.append(
|
||||||
|
" Failure: "
|
||||||
|
f"{truncate_text(failed_test_detail['failure_message'].replace(chr(10), ' | '), max_description_length)}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
for failed_test in report["failed_tests"]:
|
||||||
|
lines.append(f" Failed test: {truncate_text(failed_test, max_description_length)}")
|
||||||
else:
|
else:
|
||||||
lines.append(" Failed tests: none reported")
|
lines.append(" Failed tests: none reported")
|
||||||
|
|
||||||
|
|||||||
@ -137,12 +137,10 @@ public sealed class SettingsModelTests
|
|||||||
var migrationMapLock = lockField!.GetValue(model);
|
var migrationMapLock = lockField!.GetValue(model);
|
||||||
Assert.That(migrationMapLock, Is.Not.Null);
|
Assert.That(migrationMapLock, Is.Not.Null);
|
||||||
|
|
||||||
Task initializeTask;
|
var tasks = WithSynchronizationLockHeld(migrationMapLock!, () =>
|
||||||
Task registerTask;
|
|
||||||
lock (migrationMapLock!)
|
|
||||||
{
|
{
|
||||||
initializeTask = Task.Run(() => model.InitializeAsync());
|
var initializeTask = Task.Run(() => model.InitializeAsync());
|
||||||
registerTask = Task.Run(() => model.RegisterMigration(new TestLatestSettingsMigrationV2ToV3()));
|
var registerTask = Task.Run(() => model.RegisterMigration(new TestLatestSettingsMigrationV2ToV3()));
|
||||||
|
|
||||||
Thread.Sleep(50);
|
Thread.Sleep(50);
|
||||||
|
|
||||||
@ -151,7 +149,11 @@ public sealed class SettingsModelTests
|
|||||||
Assert.That(initializeTask.IsCompleted, Is.False);
|
Assert.That(initializeTask.IsCompleted, Is.False);
|
||||||
Assert.That(registerTask.IsCompleted, Is.False);
|
Assert.That(registerTask.IsCompleted, Is.False);
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
return (initializeTask, registerTask);
|
||||||
|
});
|
||||||
|
|
||||||
|
var (initializeTask, registerTask) = tasks;
|
||||||
|
|
||||||
await Task.WhenAll(initializeTask, registerTask);
|
await Task.WhenAll(initializeTask, registerTask);
|
||||||
|
|
||||||
@ -171,6 +173,35 @@ public sealed class SettingsModelTests
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 以与被测代码相同的同步原语持有反射获取到的锁对象,避免在 .NET 9+ 上把 <see cref="System.Threading.Lock" />
|
||||||
|
/// 退化成 <see cref="Monitor" /> 语义,导致并发测试误判。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="syncRoot">通过反射读取到的私有锁字段。</param>
|
||||||
|
/// <typeparam name="TResult">持锁代码返回的结果类型。</typeparam>
|
||||||
|
/// <param name="action">持锁期间执行的断言与并发调度逻辑。</param>
|
||||||
|
/// <returns>持锁代码的返回值。</returns>
|
||||||
|
private static TResult WithSynchronizationLockHeld<TResult>(object syncRoot, Func<TResult> action)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(syncRoot);
|
||||||
|
ArgumentNullException.ThrowIfNull(action);
|
||||||
|
|
||||||
|
#if NET9_0_OR_GREATER
|
||||||
|
if (syncRoot is System.Threading.Lock typedLock)
|
||||||
|
{
|
||||||
|
using (typedLock.EnterScope())
|
||||||
|
{
|
||||||
|
return action();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
lock (syncRoot)
|
||||||
|
{
|
||||||
|
return action();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private sealed class TestSettingsData : ISettingsData
|
private sealed class TestSettingsData : ISettingsData
|
||||||
{
|
{
|
||||||
public string Value { get; set; } = "default";
|
public string Value { get; set; } = "default";
|
||||||
|
|||||||
@ -6,12 +6,12 @@
|
|||||||
|
|
||||||
## 当前恢复点
|
## 当前恢复点
|
||||||
|
|
||||||
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-079`
|
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-080`
|
||||||
- 当前阶段:`Phase 79`
|
- 当前阶段:`Phase 80`
|
||||||
- 当前焦点:
|
- 当前焦点:
|
||||||
- `2026-04-27` 已按 PR `#295` 的 latest-head review 收口本地仍成立的问题:`AsyncExtensionsTests` 的 `ArgumentException.ParamName` 契约与 active `ai-plan` 文档过长问题
|
- `2026-04-27` 已补齐 `$gframework-pr-review` 对 GitHub Test Reporter / CTRF 测试摘要与失败用例详情的提取,当前文本输出可以直接显示失败测试名和 failure message 摘要
|
||||||
- 当前轮次已重新确认 `origin/main` 基线与 `HEAD` 同为 `617e0bf`;当前已提交 `HEAD` 的 stop metric 仍为 `30 / 50` files、`642` changed lines,本轮 PR review 同步尚未提交入该指标
|
- `SettingsModelTests.RegisterMigration_During_Cache_Rebuild_Should_Not_Leave_Stale_Type_Cache` 已按 `System.Threading.Lock` 的真实语义修正测试实现,并完成针对性 Release 测试验证
|
||||||
- 当前剩余 warning 主要集中在 `YamlConfigSchemaValidator*`、`YamlConfigLoader.cs` 与大批量 `MA0048` 文件名拆分;这些 slice 仍高于本轮 PR review 收口的低风险边界
|
- 当前剩余 warning 热点仍集中在 `YamlConfigSchemaValidator*`、`YamlConfigLoader.cs` 与大批量 `MA0048` 文件名拆分;这些 slice 仍高于本轮 PR review / test follow-up 的低风险边界
|
||||||
|
|
||||||
## 当前活跃事实
|
## 当前活跃事实
|
||||||
|
|
||||||
@ -19,12 +19,10 @@
|
|||||||
- 当前 PR review 真值:
|
- 当前 PR review 真值:
|
||||||
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output <current-pr-review-json>`
|
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output <current-pr-review-json>`
|
||||||
- 最新结果:成功;当前分支对应 PR 为 `#295`
|
- 最新结果:成功;当前分支对应 PR 为 `#295`
|
||||||
- latest-head review 在本轮修复前共有 `2` 条 open thread,分别指向 `GFramework.Core.Tests/Extensions/AsyncExtensionsTests.cs` 与 active `trace`
|
- 当前测试报告输出已能显示 `Summary` 统计、失败测试名称,以及 `Name / Failure Message` 表格中的关键信息
|
||||||
- 本轮已在本地收口上述两类问题,待提交后可重新执行 `$gframework-pr-review` 确认线程自动收口
|
- 当前直接验证结果:
|
||||||
- 提权后的直接验证当前确认为:
|
- `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~RegisterMigration_During_Cache_Rebuild_Should_Not_Leave_Stale_Type_Cache"`
|
||||||
- `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release`
|
- 最新结果:成功;`Failed: 0, Passed: 1, Skipped: 0, Total: 1`
|
||||||
- 最新结果:成功;`126 Warning(s)`、`0 Error(s)`
|
|
||||||
- 当前仍可见的既有 warning ID:`CS8766`、`CS8625`、`CS8602`、`CS8618`、`MA0048`、`MA0002`、`MA0008`
|
|
||||||
- 当前分支 stop-condition 指标:
|
- 当前分支 stop-condition 指标:
|
||||||
- `git diff --name-only refs/remotes/origin/main...HEAD | wc -l`
|
- `git diff --name-only refs/remotes/origin/main...HEAD | wc -l`
|
||||||
- 最新结果:`30`
|
- 最新结果:`30`
|
||||||
@ -33,6 +31,7 @@
|
|||||||
- 当前批次摘要:
|
- 当前批次摘要:
|
||||||
- 三轮低风险 warning 清理已在此前验证中将仓库根 warning 从 `639` 降到 `397`
|
- 三轮低风险 warning 清理已在此前验证中将仓库根 warning 从 `639` 降到 `397`
|
||||||
- 当前批次的已完成 slice 明细已迁移到归档,active todo 仅保留恢复真值
|
- 当前批次的已完成 slice 明细已迁移到归档,active todo 仅保留恢复真值
|
||||||
|
- 本轮新增内容为 PR review 工具链补强与单个失败测试修正,不扩展 warning reduction 的热点清理边界
|
||||||
- 当前建议保留到下一波次的候选:
|
- 当前建议保留到下一波次的候选:
|
||||||
- `GFramework.Game/Config/YamlConfigLoader.cs` 的 `MA0158`(单点可修,但文件本身同时承载其他高耦合 warning)
|
- `GFramework.Game/Config/YamlConfigLoader.cs` 的 `MA0158`(单点可修,但文件本身同时承载其他高耦合 warning)
|
||||||
- 测试项目中的 `MA0048` 文件名拆分波次(会显著增加 changed-file 数)
|
- 测试项目中的 `MA0048` 文件名拆分波次(会显著增加 changed-file 数)
|
||||||
@ -43,8 +42,8 @@
|
|||||||
- 缓解措施:本轮先避开该热点,只清理低风险且 ownership 清晰的文件集合。
|
- 缓解措施:本轮先避开该热点,只清理低风险且 ownership 清晰的文件集合。
|
||||||
- `MA0158` 迁移涉及 `net8.0` / `net9.0` / `net10.0` 多目标兼容。
|
- `MA0158` 迁移涉及 `net8.0` / `net9.0` / `net10.0` 多目标兼容。
|
||||||
- 缓解措施:复用 `StoreSelection.cs` 已存在的 `#if NET9_0_OR_GREATER` 专用锁模式,不在 `net8.0` 引入不兼容 API。
|
- 缓解措施:复用 `StoreSelection.cs` 已存在的 `#if NET9_0_OR_GREATER` 专用锁模式,不在 `net8.0` 引入不兼容 API。
|
||||||
- 当前 PR open thread 的自动收口仍依赖新提交进入远端 PR head。
|
- 当前 PR open thread 与 CI 失败信号仍依赖新提交进入远端 PR head 才能复核。
|
||||||
- 缓解措施:本轮提交后重新执行 `$gframework-pr-review`,仅以最新 head review 为准。
|
- 缓解措施:本轮提交后重新执行 `$gframework-pr-review`,同时确认 review thread 与 failed test signal 是否一起收口。
|
||||||
|
|
||||||
## 活跃文档
|
## 活跃文档
|
||||||
|
|
||||||
@ -69,6 +68,6 @@
|
|||||||
|
|
||||||
## 下一步建议
|
## 下一步建议
|
||||||
|
|
||||||
1. 提交本轮 PR review 修复与 `ai-plan` 精简同步,再重跑 `$gframework-pr-review` 验证 PR `#295` 的 open thread 是否随新 head 收口。
|
1. 提交本轮 `$gframework-pr-review` 解析增强、`SettingsModelTests` 修复与 `ai-plan` 同步。
|
||||||
2. 若后续继续推进 warning reduction,建议另开下一波次,优先明确是否接受 `YamlConfigLoader.cs` 热点触碰,或是否要专门做测试项目 `MA0048` 拆分波次。
|
2. 推送后重新执行 `$gframework-pr-review`,确认 PR `#295` 的 failed test detail 与 open thread 是否已更新为新 head 真值。
|
||||||
3. 默认在当前恢复点停下,因为继续推进已不再符合本轮“PR review 收口 + 少文件同步”的边界。
|
3. 若后续继续推进 warning reduction,建议另开下一波次处理 `YamlConfigLoader.cs` 热点或测试项目 `MA0048` 拆分波次。
|
||||||
|
|||||||
@ -1,37 +1,38 @@
|
|||||||
# Analyzer Warning Reduction 追踪
|
# Analyzer Warning Reduction 追踪
|
||||||
|
|
||||||
## 2026-04-27 — RP-079
|
## 2026-04-27 — RP-080
|
||||||
|
|
||||||
### 阶段:按 PR `#295` latest-head review 收口当前仍成立的 open thread
|
### 阶段:补强 `$gframework-pr-review` 的测试报告提取并修复 `SettingsModelTests` 失败用例
|
||||||
|
|
||||||
- 触发背景:
|
- 触发背景:
|
||||||
- 重新执行 `$gframework-pr-review` 后,确认当前分支对应的最新公开 PR 为 `#295`,而不是旧 trace 中记录的 `#291`
|
- 用户补充了 GitHub Test Reporter / CTRF 的完整 PR 评论,指出现有 `$gframework-pr-review` 输出虽然能显示 `failed=1`,但没有抓到失败测试名称与详细报错
|
||||||
- latest-head review 仍有 `2` 条 open thread,分别指向 `GFramework.Core.Tests/Extensions/AsyncExtensionsTests.cs` 的参数名契约问题,以及 active trace 过长问题
|
- PR 评论中的真实失败用例为 `RegisterMigration_During_Cache_Rebuild_Should_Not_Leave_Stale_Type_Cache`,报错集中在测试通过反射持锁后两个任务提前完成
|
||||||
- 主线程实施:
|
- 主线程实施:
|
||||||
- 修正 `ThrowShouldNotRetry` 中 `ArgumentException` 的 `ParamName` 传递逻辑,并补充断言锁定 `nameof(taskFactory)` 契约
|
- 扩展 `.agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py` 的 `parse_test_report`,增加 CTRF `Summary` 统计、紧凑 failed-tests 列表、以及 `Name` / `Failure Message` HTML 表格的提取
|
||||||
- 将 active trace 精简为单一恢复入口,并把 `RP073` 到 `RP078` 的详细过程迁入归档
|
- 更新 `gframework-pr-review` skill 文档的输出预期,明确脚本现在应提取 GitHub Test Reporter / CTRF 的失败详情
|
||||||
- 同步压缩 active todo,只保留当前恢复点真值与归档指针
|
- 修正 `GFramework.Game.Tests/Setting/SettingsModelTests.cs`,让反射取得的 `_migrationMapLock` 在 `NET9_0_OR_GREATER` 下通过 `System.Threading.Lock.EnterScope()` 持锁,而不是退化成 `Monitor` 语义
|
||||||
- 验证里程碑:
|
- 验证里程碑:
|
||||||
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output <current-pr-review-json>`
|
- `python3 -c "... parse_test_report(...)"`(基于 `/tmp/current-pr-review.json` 的现有原始评论)
|
||||||
- 结果:成功;确认 PR `#295` latest-head review 在本轮修复前共有 `2` 条 open thread
|
- 结果:成功;已能解析 `tests=2156`、`passed=2155`、`failed=1`、`duration=35.3s`,并抓到 `RegisterMigration_During_Cache_Rebuild_Should_Not_Leave_Stale_Type_Cache` 的 failure message
|
||||||
- `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release`
|
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --section tests --json-output /tmp/current-pr-review-v2.json`
|
||||||
- 结果:成功;`126 Warning(s)`、`0 Error(s)`
|
- 结果:成功;文本输出已直接显示失败测试名与报错摘要
|
||||||
- 当前可见的既有 warning ID:`CS8766`、`CS8625`、`CS8602`、`CS8618`、`MA0048`、`MA0002`、`MA0008`
|
- `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~RegisterMigration_During_Cache_Rebuild_Should_Not_Leave_Stale_Type_Cache"`
|
||||||
|
- 结果:成功;`Failed: 0, Passed: 1, Skipped: 0, Total: 1`
|
||||||
- 当前结论:
|
- 当前结论:
|
||||||
- 当前仍成立的 latest-head review 问题已经在本地收口,下一步应以新提交刷新 PR head 再复查
|
- `$gframework-pr-review` 现在已经能把 PR 测试评论里的关键失败用例信息直接提出来,不再只停留在 `failed=1`
|
||||||
- active `ai-plan` 入口现已回到“当前真值 + 归档指针”的体量,符合仓库对恢复文档的要求
|
- `SettingsModelTests` 当前失败是测试锁语义与生产代码不一致导致的误判,已在本地复现并修正通过
|
||||||
|
|
||||||
## 活跃风险
|
## 活跃风险
|
||||||
|
|
||||||
- PR review thread 是否自动关闭仍取决于新提交是否进入远端 PR head。
|
- PR 上的 latest-head review thread 与测试报告仍需要等新提交进入远端后再复核。
|
||||||
- 缓解措施:提交后重新执行 `$gframework-pr-review`,只以新的 latest-head 结果为准。
|
- 缓解措施:提交并推送后重新执行 `$gframework-pr-review`,只以新的 latest-head 和 test report 为准。
|
||||||
- `YamlConfigSchemaValidator*`、`YamlConfigLoader.cs` 与 `MA0048` 拆分仍是下一波次的高耦合候选。
|
- `YamlConfigSchemaValidator*`、`YamlConfigLoader.cs` 与 `MA0048` 拆分仍是下一波次的高耦合候选。
|
||||||
- 缓解措施:保持本轮边界只处理 PR review 收口,不顺手扩展 warning reduction 范围。
|
- 缓解措施:保持本轮边界只处理 PR review 工具链与失败测试,不顺手扩展 warning reduction 范围。
|
||||||
|
|
||||||
## 下一步
|
## 下一步
|
||||||
|
|
||||||
1. 完成本轮验证并提交。
|
1. 完成本轮提交。
|
||||||
2. 重新执行 `$gframework-pr-review`,确认 PR `#295` latest-head 是否还有 unresolved thread。
|
2. 推送后重新执行 `$gframework-pr-review`,确认 PR `#295` 的 failed test detail 与 unresolved thread 是否已刷新。
|
||||||
|
|
||||||
## 历史归档指针
|
## 历史归档指针
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user