abapgit-agent 1.17.5 → 1.17.7

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "abapgit-agent",
3
- "version": "1.17.5",
3
+ "version": "1.17.7",
4
4
  "description": "ABAP Git Agent - Pull and activate ABAP code via abapGit from any git repository",
5
5
  "files": [
6
6
  "bin/",
@@ -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
- // Build a set of failed method names for quick lookup
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
- testcases.push(
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
- // Emit empty <testcase> elements for passing tests (Jenkins shows total count)
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
- testcases.push(
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
- testcases.push(` <testcase name="(no tests)" classname="${escapeXml(className)}"/>`);
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
- testcases.join('\n') + '\n' +
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>,... [--coverage] [--junit-output <file>] [--json]
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,...> Comma-separated .testclasses.abap files (required).
246
- --coverage Include code coverage data in output.
247
- --junit-output <file> Write results as JUnit XML to this file.
248
- --json Output as JSON.
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
- // Check for coverage option
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;
@@ -311,7 +327,46 @@ Examples:
311
327
  }
312
328
  }
313
329
 
314
- // JUnit output modewrite XML, then continue to normal output
330
+ // Coverage threshold enforcementaggregated across all files
331
+ // Runs BEFORE JUnit output so the synthetic failure testcase lands in the XML.
332
+ if (coverage && coverageThreshold > 0) {
333
+ 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);
334
+ 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);
335
+
336
+ if (totalLines === 0) {
337
+ if (!jsonOutput) console.warn('⚠️ Coverage data unavailable — threshold not enforced');
338
+ } else {
339
+ const rate = Math.round((coveredLines / totalLines) * 100);
340
+ if (rate < coverageThreshold) {
341
+ const msg = `Coverage ${rate}% is below threshold ${coverageThreshold}%`;
342
+ if (coverageMode === 'warn') {
343
+ if (!jsonOutput) console.warn(`⚠️ ${msg}`);
344
+ } else {
345
+ if (!jsonOutput) console.error(`❌ ${msg}`);
346
+ hasErrors = true;
347
+ // Inject a synthetic failing testcase so the JUnit report shows the failure,
348
+ // not just a passing test badge alongside a red build.
349
+ results.push({
350
+ _className: 'Coverage',
351
+ _synthetic: true,
352
+ TEST_COUNT: 1,
353
+ PASSED_COUNT: 0,
354
+ FAILED_COUNT: 1,
355
+ ERRORS: [{
356
+ CLASS_NAME: 'Coverage',
357
+ METHOD_NAME: 'coverage_threshold',
358
+ ERROR_KIND: 'FAILURE',
359
+ ERROR_TEXT: msg
360
+ }]
361
+ });
362
+ }
363
+ } else {
364
+ if (!jsonOutput) console.log(`✅ Coverage ${rate}% meets threshold ${coverageThreshold}%`);
365
+ }
366
+ }
367
+ }
368
+
369
+ // JUnit output mode — write XML after threshold check so synthetic failure is included
315
370
  if (junitOutput) {
316
371
  const xml = buildUnitJUnit(results);
317
372
  const outputPath = pathModule.isAbsolute(junitOutput)