dsh-flakefinder 0.2.0 → 0.2.2
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.en.md +1 -1
- package/README.md +3 -3
- package/lib/args.js +12 -2
- package/lib/parse.js +25 -9
- package/lib/runner.js +4 -4
- package/package.json +2 -2
package/README.en.md
CHANGED
|
@@ -18,7 +18,7 @@ A DeepSeek Harness plugin for test stability: run tests repeatedly, classify fla
|
|
|
18
18
|
| `flaky_quarantine` | Write flaky cases to `.flakefinder.json` | yes (approval) |
|
|
19
19
|
| `flaky_clear` | Remove recovered cases from the manifest | yes (approval) |
|
|
20
20
|
|
|
21
|
-
Frameworks: vitest / jest / pytest / node:test, auto-detected in that order.
|
|
21
|
+
Frameworks: vitest / jest / pytest (setup.cfg / pyproject / pytest.ini / tox.ini) / node:test, auto-detected in that order. JUnit XML entities and TAP SKIP directives are parsed.
|
|
22
22
|
|
|
23
23
|
## Install
|
|
24
24
|
|
package/README.md
CHANGED
|
@@ -22,8 +22,8 @@ DeepSeek Harness 测试稳定性插件:支持 vitest / jest / pytest / node:te
|
|
|
22
22
|
|
|
23
23
|
- vitest:读取 `node_modules/vitest/vitest.mjs`,使用 JSON reporter
|
|
24
24
|
- jest:读取 `node_modules/jest/bin/jest.js`,使用 `--json --outputFile`
|
|
25
|
-
- pytest:`python -m pytest --junitxml`,解析 JUnit XML
|
|
26
|
-
- node:test:`node --test --test-reporter=tap`,解析 TAP
|
|
25
|
+
- pytest:`python -m pytest --junitxml`,解析 JUnit XML(支持 setup.cfg / pyproject / pytest.ini / tox.ini 探测与 XML 实体解码)
|
|
26
|
+
- node:test:`node --test --test-reporter=tap`,解析 TAP(含 SKIP 指令归类)
|
|
27
27
|
- `framework: auto` 按 vitest → jest → pytest → node:test 自动探测
|
|
28
28
|
|
|
29
29
|
## 安装
|
|
@@ -87,7 +87,7 @@ Agent:
|
|
|
87
87
|
|
|
88
88
|
- Node >= 22.13,TypeScript,零运行时依赖
|
|
89
89
|
- 测试进程走 DSH 官方 subprocess 服务,argv 数组、无 shell
|
|
90
|
-
- 全量单测
|
|
90
|
+
- 全量单测 40+:解析、判定、存储、审批门、pytest 计划、subprocess 超时、注册与 manifest
|
|
91
91
|
- `pnpm test`:构建 + `node --test`
|
|
92
92
|
|
|
93
93
|
## 发布门禁
|
package/lib/args.js
CHANGED
|
@@ -25,10 +25,20 @@ function hasPytestConfig(cwd) {
|
|
|
25
25
|
if (fs.existsSync(path.join(cwd, 'pytest.ini')) || fs.existsSync(path.join(cwd, 'tox.ini')))
|
|
26
26
|
return true;
|
|
27
27
|
const pyproject = path.join(cwd, 'pyproject.toml');
|
|
28
|
-
if (
|
|
28
|
+
if (fs.existsSync(pyproject)) {
|
|
29
|
+
try {
|
|
30
|
+
if (/\[tool\.pytest(\.ini_options)?\]/.test(fs.readFileSync(pyproject, 'utf8')))
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// 读不了就当没有配置,继续探测 setup.cfg。
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const setupCfg = path.join(cwd, 'setup.cfg');
|
|
38
|
+
if (!fs.existsSync(setupCfg))
|
|
29
39
|
return false;
|
|
30
40
|
try {
|
|
31
|
-
return /\[tool
|
|
41
|
+
return /\[tool:pytest\]/.test(fs.readFileSync(setupCfg, 'utf8'));
|
|
32
42
|
}
|
|
33
43
|
catch {
|
|
34
44
|
return false;
|
package/lib/parse.js
CHANGED
|
@@ -73,9 +73,22 @@ function reportedCount(reported, fallback) {
|
|
|
73
73
|
return fallback;
|
|
74
74
|
}
|
|
75
75
|
function attrOf(tag, name) {
|
|
76
|
-
const m = new RegExp(name + '="([^"]*)"').exec(tag);
|
|
76
|
+
const m = new RegExp('(?:^|\\s)' + name + '="([^"]*)"').exec(tag);
|
|
77
77
|
return m === null ? '' : m[1];
|
|
78
78
|
}
|
|
79
|
+
/** 解码 XML 属性里的实体与数字字符引用。 */
|
|
80
|
+
function decodeXmlEntities(text) {
|
|
81
|
+
const named = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'" };
|
|
82
|
+
return text.replace(/&(#x?[0-9a-f]+|[a-z]+);/gi, (raw, body) => {
|
|
83
|
+
const key = body.toLowerCase();
|
|
84
|
+
if (key.startsWith('#')) {
|
|
85
|
+
const hex = key.startsWith('#x');
|
|
86
|
+
const code = parseInt(hex ? key.slice(2) : key.slice(1), hex ? 16 : 10);
|
|
87
|
+
return Number.isFinite(code) ? String.fromCodePoint(code) : raw;
|
|
88
|
+
}
|
|
89
|
+
return named[key] ?? raw;
|
|
90
|
+
});
|
|
91
|
+
}
|
|
79
92
|
/** 解析 pytest 的 JUnit XML 报告。 */
|
|
80
93
|
export function parseJunitReport(text) {
|
|
81
94
|
const tests = [];
|
|
@@ -84,15 +97,15 @@ export function parseJunitReport(text) {
|
|
|
84
97
|
for (const m of text.matchAll(block)) {
|
|
85
98
|
const tag = m[1];
|
|
86
99
|
const body = m[2];
|
|
87
|
-
const classname = attrOf(tag, 'classname') || '(pytest)';
|
|
88
|
-
const name = attrOf(tag, 'name') || '(unnamed)';
|
|
100
|
+
const classname = decodeXmlEntities(attrOf(tag, 'classname')) || '(pytest)';
|
|
101
|
+
const name = decodeXmlEntities(attrOf(tag, 'name')) || '(unnamed)';
|
|
89
102
|
const status = /<(?:failure|error)\b/.test(body) ? 'failed' : /<skipped\b/.test(body) ? 'skipped' : 'passed';
|
|
90
103
|
tests.push({ id: classname + ' > ' + name, file: classname, name, status, durationMs: Math.max(0, Number(attrOf(tag, 'time')) || 0) });
|
|
91
104
|
}
|
|
92
105
|
for (const m of text.matchAll(single)) {
|
|
93
106
|
const tag = m[1];
|
|
94
|
-
const classname = attrOf(tag, 'classname') || '(pytest)';
|
|
95
|
-
const name = attrOf(tag, 'name') || '(unnamed)';
|
|
107
|
+
const classname = decodeXmlEntities(attrOf(tag, 'classname')) || '(pytest)';
|
|
108
|
+
const name = decodeXmlEntities(attrOf(tag, 'name')) || '(unnamed)';
|
|
96
109
|
tests.push({ id: classname + ' > ' + name, file: classname, name, status: 'passed', durationMs: Math.max(0, Number(attrOf(tag, 'time')) || 0) });
|
|
97
110
|
}
|
|
98
111
|
const failed = tests.filter(item => item.status === 'failed').length;
|
|
@@ -110,23 +123,26 @@ export function parseTapReport(text) {
|
|
|
110
123
|
continue;
|
|
111
124
|
const failed = match[1] === 'not ok';
|
|
112
125
|
index += 1;
|
|
113
|
-
const
|
|
126
|
+
const rawName = match[3] ?? '(unnamed)';
|
|
127
|
+
const skipped = /#\s*SKIP\b/i.test(rawName);
|
|
128
|
+
const name = rawName.replace(/\s+#.*$/, '');
|
|
114
129
|
tests.push({
|
|
115
130
|
id: 'node:' + String(index) + ' > ' + name,
|
|
116
131
|
file: '(node:test)',
|
|
117
132
|
name,
|
|
118
|
-
status: failed ? 'failed' : 'passed',
|
|
133
|
+
status: skipped ? 'skipped' : failed ? 'failed' : 'passed',
|
|
119
134
|
durationMs: 0,
|
|
120
135
|
});
|
|
121
136
|
}
|
|
122
137
|
const failed = tests.filter(item => item.status === 'failed').length;
|
|
138
|
+
const skipped = tests.filter(item => item.status === 'skipped').length;
|
|
123
139
|
return {
|
|
124
140
|
framework: 'node',
|
|
125
141
|
success: failed === 0 && tests.length > 0,
|
|
126
142
|
total: tests.length,
|
|
127
|
-
passed: tests.length - failed,
|
|
143
|
+
passed: tests.length - failed - skipped,
|
|
128
144
|
failed,
|
|
129
|
-
skipped
|
|
145
|
+
skipped,
|
|
130
146
|
tests,
|
|
131
147
|
};
|
|
132
148
|
}
|
package/lib/runner.js
CHANGED
|
@@ -26,14 +26,14 @@ export function createSubprocessRunner(spawn, graceMs, defaultTimeoutMs) {
|
|
|
26
26
|
graceMs,
|
|
27
27
|
signal: controller.signal,
|
|
28
28
|
});
|
|
29
|
+
const outcome = await handle.done;
|
|
30
|
+
const stdout = handle.collected.stdout?.readFrom(0).text ?? '';
|
|
31
|
+
const stderr = handle.collected.stderr?.readFrom(0).text ?? '';
|
|
32
|
+
return { exitCode: outcome.exitCode, signal: outcome.signal, stdout, stderr };
|
|
29
33
|
}
|
|
30
34
|
finally {
|
|
31
35
|
clearTimeout(timer);
|
|
32
36
|
}
|
|
33
|
-
const outcome = await handle.done;
|
|
34
|
-
const stdout = handle.collected.stdout?.readFrom(0).text ?? '';
|
|
35
|
-
const stderr = handle.collected.stderr?.readFrom(0).text ?? '';
|
|
36
|
-
return { exitCode: outcome.exitCode, signal: outcome.signal, stdout, stderr };
|
|
37
37
|
},
|
|
38
38
|
};
|
|
39
39
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-flakefinder",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "DSH 测试稳定性插件:flaky_detect/history/quarantine/clear/report 五工具,支持 vitest/jest/pytest/node:test,重复运行测试并识别不稳定用例,JSON 历史与隔离清单,零运行时依赖。",
|
|
3
|
+
"version": "0.2.2",
|
|
4
|
+
"description": "DSH 测试稳定性插件:flaky_detect/history/quarantine/clear/report 五工具,支持 vitest/jest/pytest/node:test,重复运行测试并识别不稳定用例,JUnit/TAP 完整解析,JSON 历史与隔离清单,零运行时依赖。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/index.d.ts",
|