abapgit-agent 1.17.5 → 1.17.6
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/package.json +1 -1
- package/src/commands/unit.js +66 -27
package/package.json
CHANGED
package/src/commands/unit.js
CHANGED
|
@@ -24,6 +24,11 @@ function escapeXml(str) {
|
|
|
24
24
|
* Maps to JUnit schema:
|
|
25
25
|
* <testsuites>
|
|
26
26
|
* <testsuite name="ZCL_MY_TEST" tests="10" failures="2" errors="0">
|
|
27
|
+
* <properties>
|
|
28
|
+
* <property name="coverage.rate" value="67"/>
|
|
29
|
+
* <property name="coverage.lines.total" value="120"/>
|
|
30
|
+
* <property name="coverage.lines.covered" value="80"/>
|
|
31
|
+
* </properties>
|
|
27
32
|
* <testcase name="TEST_METHOD_1" classname="ZCL_MY_TEST"/>
|
|
28
33
|
* <testcase name="TEST_METHOD_2" classname="ZCL_MY_TEST">
|
|
29
34
|
* <failure type="FAILURE" message="...">detail</failure>
|
|
@@ -33,53 +38,59 @@ function escapeXml(str) {
|
|
|
33
38
|
*
|
|
34
39
|
* One testsuite per test class file. Each failed test method becomes a <failure>.
|
|
35
40
|
* Passing methods are listed as empty <testcase> elements (Jenkins counts them).
|
|
41
|
+
* Coverage stats (if present) are emitted as <properties> on the testsuite.
|
|
36
42
|
*/
|
|
37
43
|
function buildUnitJUnit(results) {
|
|
38
44
|
const suites = results.map(res => {
|
|
39
|
-
const success = res.SUCCESS || res.success;
|
|
40
45
|
const testCount = res.TEST_COUNT || res.test_count || 0;
|
|
41
46
|
const passedCount = res.PASSED_COUNT || res.passed_count || 0;
|
|
42
47
|
const failedCount = res.FAILED_COUNT || res.failed_count || 0;
|
|
43
48
|
const errors = res.ERRORS || res.errors || [];
|
|
44
49
|
const className = res._className || 'UNKNOWN'; // injected by caller
|
|
50
|
+
const coverageStats = res.COVERAGE_STATS || res.coverage_stats;
|
|
51
|
+
|
|
52
|
+
const lines = [];
|
|
53
|
+
|
|
54
|
+
// Coverage <properties> block — only emitted when coverage data is present
|
|
55
|
+
if (coverageStats) {
|
|
56
|
+
const rate = coverageStats.COVERAGE_RATE || coverageStats.coverage_rate || 0;
|
|
57
|
+
const total = coverageStats.TOTAL_LINES || coverageStats.total_lines || 0;
|
|
58
|
+
const covered = coverageStats.COVERED_LINES || coverageStats.covered_lines || 0;
|
|
59
|
+
lines.push(' <properties>');
|
|
60
|
+
lines.push(` <property name="coverage.rate" value="${rate}"/>`);
|
|
61
|
+
lines.push(` <property name="coverage.lines.total" value="${total}"/>`);
|
|
62
|
+
lines.push(` <property name="coverage.lines.covered" value="${covered}"/>`);
|
|
63
|
+
lines.push(' </properties>');
|
|
64
|
+
}
|
|
45
65
|
|
|
46
|
-
//
|
|
47
|
-
const failedMethods = new Set(
|
|
48
|
-
errors.map(e => (e.CLASS_NAME || e.class_name || '') + '=>' + (e.METHOD_NAME || e.method_name || ''))
|
|
49
|
-
);
|
|
50
|
-
|
|
51
|
-
const testcases = [];
|
|
52
|
-
|
|
53
|
-
// Emit one <testcase> per failed test
|
|
66
|
+
// One <testcase> per failed test
|
|
54
67
|
for (const err of errors) {
|
|
55
68
|
const errClassName = err.CLASS_NAME || err.class_name || className;
|
|
56
69
|
const methodName = err.METHOD_NAME || err.method_name || '?';
|
|
57
70
|
const errorKind = err.ERROR_KIND || err.error_kind || 'FAILURE';
|
|
58
71
|
const errorText = err.ERROR_TEXT || err.error_text || 'Test failed';
|
|
59
|
-
|
|
72
|
+
lines.push(
|
|
60
73
|
` <testcase name="${escapeXml(methodName)}" classname="${escapeXml(errClassName)}">\n` +
|
|
61
74
|
` <failure type="${escapeXml(errorKind)}" message="${escapeXml(errorText)}">${escapeXml(errorText)}</failure>\n` +
|
|
62
75
|
` </testcase>`
|
|
63
76
|
);
|
|
64
77
|
}
|
|
65
78
|
|
|
66
|
-
//
|
|
67
|
-
// We can't enumerate them individually (ABAP doesn't return passing method names),
|
|
68
|
-
// so emit one aggregate passing testcase when passedCount > 0
|
|
79
|
+
// Aggregate passing testcase (ABAP doesn't return individual passing method names)
|
|
69
80
|
if (passedCount > 0) {
|
|
70
|
-
|
|
81
|
+
lines.push(
|
|
71
82
|
` <testcase name="(${passedCount} passing test(s))" classname="${escapeXml(className)}"/>`
|
|
72
83
|
);
|
|
73
84
|
}
|
|
74
85
|
|
|
75
86
|
if (testCount === 0) {
|
|
76
|
-
|
|
87
|
+
lines.push(` <testcase name="(no tests)" classname="${escapeXml(className)}"/>`);
|
|
77
88
|
}
|
|
78
89
|
|
|
79
90
|
return (
|
|
80
91
|
` <testsuite name="${escapeXml(className)}" ` +
|
|
81
92
|
`tests="${Math.max(testCount, 1)}" failures="${failedCount}" errors="0">\n` +
|
|
82
|
-
|
|
93
|
+
lines.join('\n') + '\n' +
|
|
83
94
|
` </testsuite>`
|
|
84
95
|
);
|
|
85
96
|
});
|
|
@@ -235,21 +246,23 @@ module.exports = {
|
|
|
235
246
|
if (args.includes('--help') || args.includes('-h')) {
|
|
236
247
|
console.log(`
|
|
237
248
|
Usage:
|
|
238
|
-
abapgit-agent unit --files <file1>,<file2>,... [
|
|
249
|
+
abapgit-agent unit --files <file1>,<file2>,... [options]
|
|
239
250
|
|
|
240
251
|
Description:
|
|
241
252
|
Run AUnit tests for ABAP test class files (.testclasses.abap).
|
|
242
253
|
Objects must be already active in the ABAP system (run pull first).
|
|
243
254
|
|
|
244
255
|
Parameters:
|
|
245
|
-
--files <file1,...>
|
|
246
|
-
--coverage
|
|
247
|
-
--
|
|
248
|
-
--
|
|
256
|
+
--files <file1,...> Comma-separated .testclasses.abap files (required).
|
|
257
|
+
--coverage Include code coverage data in output.
|
|
258
|
+
--coverage-threshold <N> Fail/warn when coverage is below N percent (0–100). Default: 0 (off).
|
|
259
|
+
--coverage-mode <warn|fail> Action when below threshold: warn = UNSTABLE, fail = error. Default: fail.
|
|
260
|
+
--junit-output <file> Write results as JUnit XML to this file.
|
|
261
|
+
--json Output as JSON.
|
|
249
262
|
|
|
250
263
|
Examples:
|
|
251
264
|
abapgit-agent unit --files src/zcl_my_test.clas.testclasses.abap
|
|
252
|
-
abapgit-agent unit --files src/zcl_my_test.clas.testclasses.abap --coverage
|
|
265
|
+
abapgit-agent unit --files src/zcl_my_test.clas.testclasses.abap --coverage --coverage-threshold 80
|
|
253
266
|
abapgit-agent unit --files src/zcl_my_test.clas.testclasses.abap --junit-output reports/unit.xml
|
|
254
267
|
`);
|
|
255
268
|
return;
|
|
@@ -260,18 +273,21 @@ Examples:
|
|
|
260
273
|
const filesArgIndex = args.indexOf('--files');
|
|
261
274
|
if (filesArgIndex === -1 || filesArgIndex + 1 >= args.length) {
|
|
262
275
|
console.error('Error: --files parameter required');
|
|
263
|
-
console.error('Usage: abapgit-agent unit --files <file1>,<file2>,... [--coverage] [--junit-output <file>] [--json]');
|
|
264
|
-
console.error('Example: abapgit-agent unit --files src/zcl_my_test.clas.testclasses.abap');
|
|
265
|
-
console.error('Example: abapgit-agent unit --files src/zcl_my_test.clas.testclasses.abap --coverage');
|
|
266
|
-
console.error('Example: abapgit-agent unit --files src/zcl_my_test.clas.testclasses.abap --junit-output reports/unit.xml');
|
|
276
|
+
console.error('Usage: abapgit-agent unit --files <file1>,<file2>,... [--coverage] [--coverage-threshold <N>] [--junit-output <file>] [--json]');
|
|
267
277
|
process.exit(1);
|
|
268
278
|
}
|
|
269
279
|
|
|
270
280
|
const files = args[filesArgIndex + 1].split(',').map(f => f.trim());
|
|
271
281
|
|
|
272
|
-
//
|
|
282
|
+
// Coverage options
|
|
273
283
|
const coverage = args.includes('--coverage');
|
|
274
284
|
|
|
285
|
+
const coverageThresholdIdx = args.indexOf('--coverage-threshold');
|
|
286
|
+
const coverageThreshold = coverageThresholdIdx !== -1 ? parseInt(args[coverageThresholdIdx + 1], 10) : 0;
|
|
287
|
+
|
|
288
|
+
const coverageModeIdx = args.indexOf('--coverage-mode');
|
|
289
|
+
const coverageMode = coverageModeIdx !== -1 ? args[coverageModeIdx + 1] : 'fail';
|
|
290
|
+
|
|
275
291
|
// Parse optional --junit-output parameter
|
|
276
292
|
const junitArgIndex = args.indexOf('--junit-output');
|
|
277
293
|
const junitOutput = junitArgIndex !== -1 ? args[junitArgIndex + 1] : null;
|
|
@@ -327,6 +343,29 @@ Examples:
|
|
|
327
343
|
}
|
|
328
344
|
}
|
|
329
345
|
|
|
346
|
+
// Coverage threshold enforcement — aggregated across all files
|
|
347
|
+
if (coverage && coverageThreshold > 0) {
|
|
348
|
+
const totalLines = results.reduce((s, r) => s + ((r.COVERAGE_STATS || r.coverage_stats)?.TOTAL_LINES || (r.COVERAGE_STATS || r.coverage_stats)?.total_lines || 0), 0);
|
|
349
|
+
const coveredLines = results.reduce((s, r) => s + ((r.COVERAGE_STATS || r.coverage_stats)?.COVERED_LINES || (r.COVERAGE_STATS || r.coverage_stats)?.covered_lines || 0), 0);
|
|
350
|
+
|
|
351
|
+
if (totalLines === 0) {
|
|
352
|
+
if (!jsonOutput) console.warn('⚠️ Coverage data unavailable — threshold not enforced');
|
|
353
|
+
} else {
|
|
354
|
+
const rate = Math.round((coveredLines / totalLines) * 100);
|
|
355
|
+
if (rate < coverageThreshold) {
|
|
356
|
+
const msg = `Coverage ${rate}% is below threshold ${coverageThreshold}%`;
|
|
357
|
+
if (coverageMode === 'warn') {
|
|
358
|
+
if (!jsonOutput) console.warn(`⚠️ ${msg}`);
|
|
359
|
+
} else {
|
|
360
|
+
if (!jsonOutput) console.error(`❌ ${msg}`);
|
|
361
|
+
hasErrors = true;
|
|
362
|
+
}
|
|
363
|
+
} else {
|
|
364
|
+
if (!jsonOutput) console.log(`✅ Coverage ${rate}% meets threshold ${coverageThreshold}%`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
330
369
|
// JSON output mode
|
|
331
370
|
if (jsonOutput) {
|
|
332
371
|
console.log(JSON.stringify(results, null, 2));
|