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 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(内置能力,无需 pytest 插件)
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
- - 全量单测 35+:解析、判定、存储、审批门、pytest 计划、注册与 manifest
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 (!fs.existsSync(pyproject))
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\.pytest(\.ini_options)?\]/.test(fs.readFileSync(pyproject, 'utf8'));
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 name = (match[3] ?? '(unnamed)').replace(/\s+#.*$/, '');
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: 0,
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.0",
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",