abapgit-agent 1.18.2 → 1.19.1
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/abap/guidelines/abapgit-xml-only.md +63 -3
- package/package.json +2 -1
- package/src/commands/inspect.js +178 -72
- package/src/commands/pull.js +44 -1
- package/src/commands/unit.js +128 -33
- package/src/commands/view.js +46 -1
|
@@ -6,9 +6,9 @@ parent: ABAP Coding Guidelines
|
|
|
6
6
|
grand_parent: ABAP Development
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
# abapGit XML Metadata — XML-Only Objects (TABL, STRU, DTEL, TTYP, DOMA, MSAG)
|
|
9
|
+
# abapGit XML Metadata — XML-Only Objects (TABL, STRU, DTEL, TTYP, DOMA, MSAG, SUSO)
|
|
10
10
|
|
|
11
|
-
XML templates for ABAP Dictionary objects that have no `.abap` source file — TABL, STRU, DTEL, TTYP, DOMA,
|
|
11
|
+
XML templates for ABAP Dictionary objects that have no `.abap` source file — TABL, STRU, DTEL, TTYP, DOMA, message classes (MSAG), and authorization objects (SUSO).
|
|
12
12
|
|
|
13
13
|
> **CRITICAL: Always write XML files with a UTF-8 BOM (`\ufeff`) as the very first character**, before `<?xml ...`.
|
|
14
14
|
> Without the BOM, abapGit shows the object as **"M" (modified)** after every pull.
|
|
@@ -19,7 +19,7 @@ XML templates for ABAP Dictionary objects that have no `.abap` source file — T
|
|
|
19
19
|
> **For CLAS, INTF, PROG, DDLS, DCLS, FUGR** (have source files): see `abapgit-agent ref --topic abapgit`
|
|
20
20
|
> **For DDLS, DCLS** (CDS — have source files): also see `abapgit-agent ref --topic abapgit`
|
|
21
21
|
|
|
22
|
-
**Searchable keywords**: table xml, structure xml, data element xml, table type xml, domain xml, message class xml, tabl, stru, dtel, ttyp, doma, msag, dictionary
|
|
22
|
+
**Searchable keywords**: table xml, structure xml, data element xml, table type xml, domain xml, message class xml, tabl, stru, dtel, ttyp, doma, msag, suso, authorization object, dictionary
|
|
23
23
|
|
|
24
24
|
---
|
|
25
25
|
|
|
@@ -451,3 +451,63 @@ abapGit uses the view `DD01V` as the root element (not `DD01L`). The serializer
|
|
|
451
451
|
- `T100/TEXT`: Message text — use `&1`–`&4` for placeholders (serialized as `&1`–`&4` in XML)
|
|
452
452
|
|
|
453
453
|
**Note**: The `<T100>` wrapper contains repeated `<T100>` child elements — one per message.
|
|
454
|
+
|
|
455
|
+
---
|
|
456
|
+
|
|
457
|
+
### Authorization Object (SUSO)
|
|
458
|
+
|
|
459
|
+
**Filename**: `src/zmy_suso.suso.xml`
|
|
460
|
+
|
|
461
|
+
> Pull with: `pull --files src/zmy_suso.suso.xml`
|
|
462
|
+
>
|
|
463
|
+
> View with: `view --objects ZMY_SUSO --type SUSO`
|
|
464
|
+
|
|
465
|
+
```xml
|
|
466
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
467
|
+
<abapGit version="v1.0.0" serializer="LCL_OBJECT_SUSO" serializer_version="v1.0.0">
|
|
468
|
+
<asx:abap xmlns:asx="http://www.sap.com/abapxml" version="1.0">
|
|
469
|
+
<asx:values>
|
|
470
|
+
<TOBJ>
|
|
471
|
+
<OBJCT>ZMY_SUSO</OBJCT>
|
|
472
|
+
<FIEL1>ACTVT</FIEL1>
|
|
473
|
+
<FIEL2>ZMY_FIELD</FIEL2>
|
|
474
|
+
<OCLSS>ZMY</OCLSS>
|
|
475
|
+
</TOBJ>
|
|
476
|
+
<TOBJT>
|
|
477
|
+
<LANGU>E</LANGU>
|
|
478
|
+
<OBJECT>ZMY_SUSO</OBJECT>
|
|
479
|
+
<TTEXT>My authorization object description</TTEXT>
|
|
480
|
+
</TOBJT>
|
|
481
|
+
<TOBJVORFLG>
|
|
482
|
+
<OBJCT>ZMY_SUSO</OBJCT>
|
|
483
|
+
</TOBJVORFLG>
|
|
484
|
+
<TACTZ>
|
|
485
|
+
<TACTZ>
|
|
486
|
+
<BROBJ>ZMY_SUSO</BROBJ>
|
|
487
|
+
<ACTVT>01</ACTVT>
|
|
488
|
+
</TACTZ>
|
|
489
|
+
<TACTZ>
|
|
490
|
+
<BROBJ>ZMY_SUSO</BROBJ>
|
|
491
|
+
<ACTVT>02</ACTVT>
|
|
492
|
+
</TACTZ>
|
|
493
|
+
<TACTZ>
|
|
494
|
+
<BROBJ>ZMY_SUSO</BROBJ>
|
|
495
|
+
<ACTVT>03</ACTVT>
|
|
496
|
+
</TACTZ>
|
|
497
|
+
</TACTZ>
|
|
498
|
+
</asx:values>
|
|
499
|
+
</asx:abap>
|
|
500
|
+
</abapGit>
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
**Key Fields**:
|
|
504
|
+
- `TOBJ/OBJCT`: Authorization object name — same as the filename stem
|
|
505
|
+
- `TOBJ/FIEL1`–`FIEL0`: Up to 10 authorization fields (from SU20) that make up this object
|
|
506
|
+
- `TOBJ/OCLSS`: Object class (4 chars) — groups related authorization objects (e.g. `AAAB`, `BC_A`)
|
|
507
|
+
- `TOBJT/TTEXT`: Description text
|
|
508
|
+
- `TOBJVORFLG`: Flags record — include with just `OBJCT` when no flags are set
|
|
509
|
+
- `TACTZ`: Allowed activity values — one `<TACTZ>` entry per permitted `ACTVT` value
|
|
510
|
+
|
|
511
|
+
**Note**: Authorization objects (SUSO) are the SU21 objects that group authorization fields (AUTH/SU20)
|
|
512
|
+
together. An object like `AUD_SCOPEM` uses fields `ACTVT`, `BO_SERVICE`, and `AUD_GROUP`.
|
|
513
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "abapgit-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.19.1",
|
|
4
4
|
"description": "ABAP Git Agent - Pull and activate ABAP code via abapGit from any git repository",
|
|
5
5
|
"files": [
|
|
6
6
|
"bin/",
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
"test:pull": "node tests/run-all.js --pull",
|
|
55
55
|
"test:full-pull": "node tests/run-all.js --full-pull",
|
|
56
56
|
"test:conflict": "node tests/run-all.js --conflict",
|
|
57
|
+
"test:async-pull": "node tests/run-all.js --async-pull",
|
|
57
58
|
"pull": "node bin/abapgit-agent",
|
|
58
59
|
"release": "node scripts/release.js",
|
|
59
60
|
"unrelease": "node scripts/unrelease.js"
|
package/src/commands/inspect.js
CHANGED
|
@@ -24,15 +24,17 @@ function escapeXml(str) {
|
|
|
24
24
|
* Maps to JUnit schema:
|
|
25
25
|
* <testsuites>
|
|
26
26
|
* <testsuite name="CLAS ZCL_MY_CLASS" tests="N" failures="F" errors="0">
|
|
27
|
-
* <testcase name="Syntax check" classname="ZCL_MY_CLASS"
|
|
28
|
-
*
|
|
27
|
+
* <testcase name="Inspect / Syntax check" classname="ZCL_MY_CLASS"/> ← clean
|
|
28
|
+
* <testcase name="Inspect / CL_CI_TEST_EXCEPTION/UNHANDLED_01" ← finding
|
|
29
|
+
* classname="ZCL_MY_CLASS">
|
|
30
|
+
* <failure type="SyntaxError" message="...">method/line/detail</failure>
|
|
29
31
|
* </testcase>
|
|
30
32
|
* </testsuite>
|
|
31
33
|
* </testsuites>
|
|
32
34
|
*
|
|
33
|
-
* One testsuite per object. Each error becomes
|
|
34
|
-
*
|
|
35
|
-
*
|
|
35
|
+
* One testsuite per object. Each error/warning becomes its own testcase named
|
|
36
|
+
* after the SCI check rule (check_class/check_code) so Jenkins shows exactly
|
|
37
|
+
* which rule fired. Clean objects get a single passing testcase.
|
|
36
38
|
*/
|
|
37
39
|
function buildInspectJUnit(results) {
|
|
38
40
|
const suites = results.map(res => {
|
|
@@ -42,49 +44,56 @@ function buildInspectJUnit(results) {
|
|
|
42
44
|
const warnings = res.WARNINGS !== undefined ? res.WARNINGS : (res.warnings || []);
|
|
43
45
|
const errorCount = errors.length;
|
|
44
46
|
const warnCount = warnings.length;
|
|
45
|
-
// One testcase per error/warning; at least one testcase for a clean object
|
|
46
47
|
const testCount = Math.max(1, errorCount + warnCount);
|
|
47
48
|
|
|
48
49
|
const testcases = [];
|
|
49
50
|
|
|
51
|
+
const inspectClass = `${escapeXml(objectName)}.Inspect`;
|
|
52
|
+
|
|
50
53
|
if (errorCount === 0 && warnCount === 0) {
|
|
51
|
-
testcases.push(` <testcase name="Syntax check" classname="${
|
|
54
|
+
testcases.push(` <testcase name="Syntax check" classname="${inspectClass}"/>`);
|
|
52
55
|
}
|
|
53
56
|
|
|
54
57
|
for (const err of errors) {
|
|
55
|
-
const line = err.LINE
|
|
56
|
-
const column = err.COLUMN
|
|
57
|
-
const text = err.TEXT
|
|
58
|
+
const line = err.LINE || err.line || '?';
|
|
59
|
+
const column = err.COLUMN || err.column || '?';
|
|
60
|
+
const text = err.TEXT || err.text || 'Unknown error';
|
|
58
61
|
const methodName = err.METHOD_NAME || err.method_name;
|
|
59
|
-
const sobjname = err.SOBJNAME
|
|
62
|
+
const sobjname = err.SOBJNAME || err.sobjname || '';
|
|
63
|
+
const checkClass = err.CHECK_CLASS || err.check_class || '';
|
|
64
|
+
const checkCode = err.CHECK_CODE || err.check_code || '';
|
|
60
65
|
const detail = [
|
|
61
66
|
methodName ? `Method: ${methodName}` : null,
|
|
62
67
|
`Line ${line}, Column ${column}`,
|
|
63
68
|
sobjname ? `Include: ${sobjname}` : null,
|
|
64
69
|
text
|
|
65
70
|
].filter(Boolean).join('\n');
|
|
66
|
-
const
|
|
71
|
+
const checkId = checkClass && checkCode ? `${checkClass}/${checkCode}` : null;
|
|
72
|
+
const caseName = checkId ? checkId : (methodName ? `${methodName} line ${line}` : `Line ${line}`);
|
|
67
73
|
testcases.push(
|
|
68
|
-
` <testcase name="${escapeXml(caseName)}" classname="${
|
|
74
|
+
` <testcase name="${escapeXml(caseName)}" classname="${inspectClass}">\n` +
|
|
69
75
|
` <failure type="SyntaxError" message="${escapeXml(text)}">${escapeXml(detail)}</failure>\n` +
|
|
70
76
|
` </testcase>`
|
|
71
77
|
);
|
|
72
78
|
}
|
|
73
79
|
|
|
74
80
|
for (const warn of warnings) {
|
|
75
|
-
const line = warn.LINE
|
|
76
|
-
const text = warn.MESSAGE
|
|
81
|
+
const line = warn.LINE || warn.line || '?';
|
|
82
|
+
const text = warn.MESSAGE || warn.message || warn.TEXT || warn.text || 'Warning';
|
|
77
83
|
const methodName = warn.METHOD_NAME || warn.method_name;
|
|
78
|
-
const sobjname = warn.SOBJNAME
|
|
84
|
+
const sobjname = warn.SOBJNAME || warn.sobjname || '';
|
|
85
|
+
const checkClass = warn.CHECK_CLASS || warn.check_class || '';
|
|
86
|
+
const checkCode = warn.CHECK_CODE || warn.check_code || '';
|
|
79
87
|
const detail = [
|
|
80
88
|
methodName ? `Method: ${methodName}` : null,
|
|
81
89
|
`Line ${line}`,
|
|
82
90
|
sobjname ? `Include: ${sobjname}` : null,
|
|
83
91
|
text
|
|
84
92
|
].filter(Boolean).join('\n');
|
|
85
|
-
const
|
|
93
|
+
const checkId = checkClass && checkCode ? `${checkClass}/${checkCode}` : null;
|
|
94
|
+
const caseName = checkId ? checkId : (methodName ? `${methodName} line ${line}` : `Line ${line}`);
|
|
86
95
|
testcases.push(
|
|
87
|
-
` <testcase name="${escapeXml(caseName)}" classname="${
|
|
96
|
+
` <testcase name="${escapeXml(caseName)}" classname="${inspectClass}">\n` +
|
|
88
97
|
` <failure type="Warning" message="${escapeXml(text)}">${escapeXml(detail)}</failure>\n` +
|
|
89
98
|
` </testcase>`
|
|
90
99
|
);
|
|
@@ -107,8 +116,10 @@ function buildInspectJUnit(results) {
|
|
|
107
116
|
}
|
|
108
117
|
|
|
109
118
|
/**
|
|
110
|
-
* Inspect
|
|
119
|
+
* Inspect a single batch of files in one request (max CHUNK_SIZE files)
|
|
111
120
|
*/
|
|
121
|
+
const CHUNK_SIZE = 10;
|
|
122
|
+
|
|
112
123
|
async function inspectAllFiles(files, csrfToken, config, variant, http, verbose = false) {
|
|
113
124
|
// Convert files to uppercase names
|
|
114
125
|
const fileNames = files.map(f => {
|
|
@@ -152,6 +163,66 @@ async function inspectAllFiles(files, csrfToken, config, variant, http, verbose
|
|
|
152
163
|
}
|
|
153
164
|
}
|
|
154
165
|
|
|
166
|
+
/**
|
|
167
|
+
* Apply inspect.suppress rules to a single result — downgrade matching errors/warnings to infos.
|
|
168
|
+
*/
|
|
169
|
+
function applySuppressRules(result, suppressRules) {
|
|
170
|
+
if (!suppressRules || suppressRules.length === 0) return;
|
|
171
|
+
const objName = (result.OBJECT_NAME || result.object_name || '').toUpperCase();
|
|
172
|
+
for (const rule of suppressRules) {
|
|
173
|
+
const objPattern = new RegExp('^' + (rule.object || '*').toUpperCase().replace(/\*/g, '.*') + '$');
|
|
174
|
+
if (!objPattern.test(objName)) continue;
|
|
175
|
+
const msgPattern = new RegExp((rule.message || '*').replace(/\*/g, '.*'), 'i');
|
|
176
|
+
|
|
177
|
+
const errors = result.ERRORS || result.errors || [];
|
|
178
|
+
const kept = [];
|
|
179
|
+
for (const err of errors) {
|
|
180
|
+
const text = err.TEXT || err.text || '';
|
|
181
|
+
if (msgPattern.test(text)) {
|
|
182
|
+
const infos = result.INFOS || result.infos || [];
|
|
183
|
+
infos.push({ ...err, MESSAGE: `[suppressed] ${text}`, message: `[suppressed] ${text}` });
|
|
184
|
+
if (result.INFOS !== undefined) result.INFOS = infos; else result.infos = infos;
|
|
185
|
+
const ec = result.ERROR_COUNT !== undefined ? 'ERROR_COUNT' : 'error_count';
|
|
186
|
+
result[ec] = Math.max(0, (result[ec] || 0) - 1);
|
|
187
|
+
} else {
|
|
188
|
+
kept.push(err);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (result.ERRORS !== undefined) result.ERRORS = kept; else result.errors = kept;
|
|
192
|
+
|
|
193
|
+
const warnings = result.WARNINGS || result.warnings || [];
|
|
194
|
+
const keptW = [];
|
|
195
|
+
for (const warn of warnings) {
|
|
196
|
+
const text = warn.MESSAGE || warn.message || '';
|
|
197
|
+
if (msgPattern.test(text)) {
|
|
198
|
+
const infos = result.INFOS || result.infos || [];
|
|
199
|
+
infos.push({ ...warn, MESSAGE: `[suppressed] ${text}`, message: `[suppressed] ${text}` });
|
|
200
|
+
if (result.INFOS !== undefined) result.INFOS = infos; else result.infos = infos;
|
|
201
|
+
} else {
|
|
202
|
+
keptW.push(warn);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (result.WARNINGS !== undefined) result.WARNINGS = keptW; else result.warnings = keptW;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Split files into chunks and inspect each sequentially, combining results.
|
|
211
|
+
* Calls onChunkResult(result) for each object result as chunks complete.
|
|
212
|
+
*/
|
|
213
|
+
async function inspectInChunks(files, csrfToken, config, variant, http, verbose = false, onChunkResult = null) {
|
|
214
|
+
const results = [];
|
|
215
|
+
for (let i = 0; i < files.length; i += CHUNK_SIZE) {
|
|
216
|
+
const chunk = files.slice(i, i + CHUNK_SIZE);
|
|
217
|
+
const chunkResults = await inspectAllFiles(chunk, csrfToken, config, variant, http, verbose);
|
|
218
|
+
for (const result of chunkResults) {
|
|
219
|
+
results.push(result);
|
|
220
|
+
if (onChunkResult) onChunkResult(result);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return results;
|
|
224
|
+
}
|
|
225
|
+
|
|
155
226
|
/**
|
|
156
227
|
* Process a single inspect result
|
|
157
228
|
*/
|
|
@@ -181,6 +252,8 @@ async function processInspectResult(res) {
|
|
|
181
252
|
const text = err.TEXT || err.text || 'Unknown error';
|
|
182
253
|
const methodName = err.METHOD_NAME || err.method_name;
|
|
183
254
|
const sobjname = err.SOBJNAME || err.sobjname;
|
|
255
|
+
const checkClass = err.CHECK_CLASS || err.check_class || '';
|
|
256
|
+
const checkCode = err.CHECK_CODE || err.check_code || '';
|
|
184
257
|
|
|
185
258
|
if (methodName) {
|
|
186
259
|
console.log(` Method: ${methodName}`);
|
|
@@ -190,6 +263,10 @@ async function processInspectResult(res) {
|
|
|
190
263
|
console.log(` Include: ${sobjname}`);
|
|
191
264
|
}
|
|
192
265
|
console.log(` ${text}`);
|
|
266
|
+
if (checkCode) {
|
|
267
|
+
console.log(` [${checkClass}/${checkCode}]`);
|
|
268
|
+
console.log(` → abapgit-agent inspect --doc ${checkClass}/${checkCode}`);
|
|
269
|
+
}
|
|
193
270
|
console.log('');
|
|
194
271
|
}
|
|
195
272
|
|
|
@@ -202,6 +279,8 @@ async function processInspectResult(res) {
|
|
|
202
279
|
const text = warn.MESSAGE || warn.message || 'Unknown warning';
|
|
203
280
|
const methodName = warn.METHOD_NAME || warn.method_name;
|
|
204
281
|
const sobjname = warn.SOBJNAME || warn.sobjname;
|
|
282
|
+
const checkClass = warn.CHECK_CLASS || warn.check_class || '';
|
|
283
|
+
const checkCode = warn.CHECK_CODE || warn.check_code || '';
|
|
205
284
|
|
|
206
285
|
if (methodName) {
|
|
207
286
|
console.log(` Method: ${methodName}`);
|
|
@@ -211,6 +290,10 @@ async function processInspectResult(res) {
|
|
|
211
290
|
console.log(` Include: ${sobjname}`);
|
|
212
291
|
}
|
|
213
292
|
console.log(` ${text}`);
|
|
293
|
+
if (checkCode) {
|
|
294
|
+
console.log(` [${checkClass}/${checkCode}]`);
|
|
295
|
+
console.log(` → abapgit-agent inspect --doc ${checkClass}/${checkCode}`);
|
|
296
|
+
}
|
|
214
297
|
}
|
|
215
298
|
}
|
|
216
299
|
|
|
@@ -223,6 +306,8 @@ async function processInspectResult(res) {
|
|
|
223
306
|
const text = info.MESSAGE || info.message || 'Unknown info';
|
|
224
307
|
const methodName = info.METHOD_NAME || info.method_name;
|
|
225
308
|
const sobjname = info.SOBJNAME || info.sobjname;
|
|
309
|
+
const checkClass = info.CHECK_CLASS || info.check_class || '';
|
|
310
|
+
const checkCode = info.CHECK_CODE || info.check_code || '';
|
|
226
311
|
|
|
227
312
|
if (methodName) {
|
|
228
313
|
console.log(` Method: ${methodName}`);
|
|
@@ -232,6 +317,10 @@ async function processInspectResult(res) {
|
|
|
232
317
|
console.log(` Include: ${sobjname}`);
|
|
233
318
|
}
|
|
234
319
|
console.log(` ${text}`);
|
|
320
|
+
if (checkCode) {
|
|
321
|
+
console.log(` [${checkClass}/${checkCode}]`);
|
|
322
|
+
console.log(` → abapgit-agent inspect --doc ${checkClass}/${checkCode}`);
|
|
323
|
+
}
|
|
235
324
|
}
|
|
236
325
|
}
|
|
237
326
|
} else if (success === true || success === 'X') {
|
|
@@ -254,25 +343,78 @@ module.exports = {
|
|
|
254
343
|
console.log(`
|
|
255
344
|
Usage:
|
|
256
345
|
abapgit-agent inspect --files <file1>,<file2>,... [--variant <check-variant>] [--junit-output <file>] [--json]
|
|
346
|
+
abapgit-agent inspect --doc <check_class>/<check_code>
|
|
257
347
|
|
|
258
348
|
Description:
|
|
259
349
|
Run SAP Code Inspector checks on activated ABAP objects. Requires the objects
|
|
260
350
|
to be already active in the ABAP system (run pull first).
|
|
261
351
|
|
|
352
|
+
Use --doc to fetch the SAP documentation for a specific check finding.
|
|
353
|
+
The check class and code are shown in brackets after each finding, e.g.:
|
|
354
|
+
[CL_CI_TEST_OMIT_BRACKETS/OMIT_01]
|
|
355
|
+
|
|
262
356
|
Parameters:
|
|
263
|
-
--files <file1,...> Comma-separated ABAP source files (required).
|
|
357
|
+
--files <file1,...> Comma-separated ABAP source files (required for inspection).
|
|
264
358
|
--variant <variant> Code Inspector variant (default: system default).
|
|
265
359
|
--junit-output <file> Write results as JUnit XML to this file.
|
|
266
360
|
--json Output as JSON.
|
|
361
|
+
--doc <class>/<code> Fetch documentation for a check finding.
|
|
267
362
|
|
|
268
363
|
Examples:
|
|
269
364
|
abapgit-agent inspect --files src/zcl_my_class.clas.abap
|
|
270
365
|
abapgit-agent inspect --files src/zcl_my_class.clas.abap --variant ALL_CHECKS
|
|
271
366
|
abapgit-agent inspect --files src/zcl_my_class.clas.abap --junit-output reports/inspect.xml
|
|
367
|
+
abapgit-agent inspect --doc CL_CI_TEST_OMIT_BRACKETS/OMIT_01
|
|
272
368
|
`);
|
|
273
369
|
return;
|
|
274
370
|
}
|
|
275
371
|
|
|
372
|
+
// --doc mode: fetch documentation for a check finding
|
|
373
|
+
const docArgIndex = args.indexOf('--doc');
|
|
374
|
+
if (docArgIndex !== -1) {
|
|
375
|
+
const docArg = args[docArgIndex + 1];
|
|
376
|
+
if (!docArg || !docArg.includes('/')) {
|
|
377
|
+
console.error('Error: --doc requires <check_class>/<check_code>');
|
|
378
|
+
console.error('Example: abapgit-agent inspect --doc CL_CI_TEST_OMIT_BRACKETS/OMIT_01');
|
|
379
|
+
process.exit(1);
|
|
380
|
+
}
|
|
381
|
+
const slashIdx = docArg.indexOf('/');
|
|
382
|
+
const checkClass = docArg.slice(0, slashIdx);
|
|
383
|
+
const checkCode = docArg.slice(slashIdx + 1);
|
|
384
|
+
|
|
385
|
+
const config = loadConfig();
|
|
386
|
+
const http = new AbapHttp(config);
|
|
387
|
+
const csrfToken = await http.fetchCsrfToken();
|
|
388
|
+
|
|
389
|
+
try {
|
|
390
|
+
const result = await http.post('/sap/bc/z_abapgit_agent/insp_doc', {
|
|
391
|
+
check_class: checkClass,
|
|
392
|
+
check_code: checkCode
|
|
393
|
+
}, { csrfToken });
|
|
394
|
+
|
|
395
|
+
const title = result.TITLE || result.title || '';
|
|
396
|
+
let text = result.TEXT || result.text || '';
|
|
397
|
+
|
|
398
|
+
// Strip ITF inline tags (<AB>, <EX>, </>, <DS:...>, etc.), HTML tags, and ITF placeholders (&...&)
|
|
399
|
+
text = text
|
|
400
|
+
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
|
401
|
+
.replace(/<[^>]+>/g, '')
|
|
402
|
+
.replace(/&[A-Z_]+&/g, '')
|
|
403
|
+
.replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&')
|
|
404
|
+
.replace(/ /g, ' ').replace(/ /g, ' ')
|
|
405
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
406
|
+
.trim();
|
|
407
|
+
|
|
408
|
+
if (title) console.log(`\n${title}\n${'─'.repeat(Math.min(title.length, 60))}`);
|
|
409
|
+
console.log(`\n${text || 'No documentation available.'}\n`);
|
|
410
|
+
} catch (error) {
|
|
411
|
+
const { printHttpError } = require('../utils/format-error');
|
|
412
|
+
printHttpError(error, { verbose: args.includes('--verbose') });
|
|
413
|
+
process.exit(1);
|
|
414
|
+
}
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
276
418
|
const jsonOutput = args.includes('--json');
|
|
277
419
|
const verbose = args.includes('--verbose');
|
|
278
420
|
const filesArgIndex = args.indexOf('--files');
|
|
@@ -285,7 +427,7 @@ Examples:
|
|
|
285
427
|
process.exit(1);
|
|
286
428
|
}
|
|
287
429
|
|
|
288
|
-
let filesSyntaxCheck = args[filesArgIndex + 1].split(',').map(f => f.trim());
|
|
430
|
+
let filesSyntaxCheck = [...new Set(args[filesArgIndex + 1].split(',').map(f => f.trim()))];
|
|
289
431
|
|
|
290
432
|
// Parse optional --variant parameter; fall back to project config
|
|
291
433
|
const variantArgIndex = args.indexOf('--variant');
|
|
@@ -338,51 +480,23 @@ Examples:
|
|
|
338
480
|
const http = new AbapHttp(config);
|
|
339
481
|
const csrfToken = await http.fetchCsrfToken();
|
|
340
482
|
|
|
341
|
-
// Send all files in one request
|
|
342
|
-
const results = await inspectAllFiles(filesSyntaxCheck, csrfToken, config, variant, http, verbose);
|
|
343
|
-
|
|
344
|
-
// Apply inspect.suppress rules — downgrade matching errors/warnings to infos
|
|
345
483
|
const suppressRules = inspectConfig.suppress || [];
|
|
346
|
-
|
|
484
|
+
let hasErrors = false;
|
|
485
|
+
|
|
486
|
+
// Stream results: apply suppress and print each object as its chunk completes
|
|
487
|
+
const onChunkResult = jsonOutput ? null : async (result) => {
|
|
488
|
+
applySuppressRules(result, suppressRules);
|
|
489
|
+
await processInspectResult(result);
|
|
490
|
+
const errorCount = result.ERROR_COUNT !== undefined ? result.ERROR_COUNT : (result.error_count || 0);
|
|
491
|
+
if (errorCount > 0) hasErrors = true;
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
const results = await inspectInChunks(filesSyntaxCheck, csrfToken, config, variant, http, verbose, onChunkResult);
|
|
495
|
+
|
|
496
|
+
// For JSON mode, apply suppress rules now (they were skipped during streaming)
|
|
497
|
+
if (jsonOutput) {
|
|
347
498
|
for (const result of results) {
|
|
348
|
-
|
|
349
|
-
for (const rule of suppressRules) {
|
|
350
|
-
const objPattern = new RegExp('^' + (rule.object || '*').toUpperCase().replace(/\*/g, '.*') + '$');
|
|
351
|
-
if (!objPattern.test(objName)) continue;
|
|
352
|
-
const msgPattern = new RegExp((rule.message || '*').replace(/\*/g, '.*'), 'i');
|
|
353
|
-
|
|
354
|
-
// Downgrade matching errors → infos
|
|
355
|
-
const errors = result.ERRORS || result.errors || [];
|
|
356
|
-
const kept = [];
|
|
357
|
-
for (const err of errors) {
|
|
358
|
-
const text = err.TEXT || err.text || '';
|
|
359
|
-
if (msgPattern.test(text)) {
|
|
360
|
-
const infos = result.INFOS || result.infos || [];
|
|
361
|
-
infos.push({ ...err, MESSAGE: `[suppressed] ${text}`, message: `[suppressed] ${text}` });
|
|
362
|
-
if (result.INFOS !== undefined) result.INFOS = infos; else result.infos = infos;
|
|
363
|
-
const ec = result.ERROR_COUNT !== undefined ? 'ERROR_COUNT' : 'error_count';
|
|
364
|
-
result[ec] = Math.max(0, (result[ec] || 0) - 1);
|
|
365
|
-
} else {
|
|
366
|
-
kept.push(err);
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
if (result.ERRORS !== undefined) result.ERRORS = kept; else result.errors = kept;
|
|
370
|
-
|
|
371
|
-
// Downgrade matching warnings → infos
|
|
372
|
-
const warnings = result.WARNINGS || result.warnings || [];
|
|
373
|
-
const keptW = [];
|
|
374
|
-
for (const warn of warnings) {
|
|
375
|
-
const text = warn.MESSAGE || warn.message || '';
|
|
376
|
-
if (msgPattern.test(text)) {
|
|
377
|
-
const infos = result.INFOS || result.infos || [];
|
|
378
|
-
infos.push({ ...warn, MESSAGE: `[suppressed] ${text}`, message: `[suppressed] ${text}` });
|
|
379
|
-
if (result.INFOS !== undefined) result.INFOS = infos; else result.infos = infos;
|
|
380
|
-
} else {
|
|
381
|
-
keptW.push(warn);
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
if (result.WARNINGS !== undefined) result.WARNINGS = keptW; else result.warnings = keptW;
|
|
385
|
-
}
|
|
499
|
+
applySuppressRules(result, suppressRules);
|
|
386
500
|
}
|
|
387
501
|
}
|
|
388
502
|
|
|
@@ -408,14 +522,6 @@ Examples:
|
|
|
408
522
|
return;
|
|
409
523
|
}
|
|
410
524
|
|
|
411
|
-
// Process results
|
|
412
|
-
let hasErrors = false;
|
|
413
|
-
for (const result of results) {
|
|
414
|
-
await processInspectResult(result);
|
|
415
|
-
const errorCount = result.ERROR_COUNT !== undefined ? result.ERROR_COUNT : (result.error_count || 0);
|
|
416
|
-
if (errorCount > 0) hasErrors = true;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
525
|
if (hasErrors) {
|
|
420
526
|
process.exit(1);
|
|
421
527
|
}
|
package/src/commands/pull.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
const { printHttpError } = require('../utils/format-error');
|
|
6
|
+
const { pollForCompletion, displayProgress } = require('../utils/backgroundJobPoller');
|
|
6
7
|
const fs = require('fs');
|
|
7
8
|
const pathModule = require('path');
|
|
8
9
|
const { execSync } = require('child_process');
|
|
@@ -295,7 +296,49 @@ Examples:
|
|
|
295
296
|
data.transport_request = transportRequest;
|
|
296
297
|
}
|
|
297
298
|
|
|
298
|
-
const
|
|
299
|
+
const rawResult = await http.post('/sap/bc/z_abapgit_agent/pull', data, { csrfToken });
|
|
300
|
+
|
|
301
|
+
// --- Async pull: ABAP returned 202 (job scheduled for >10 files) ---
|
|
302
|
+
const jobStatus = rawResult.STATUS || rawResult.status;
|
|
303
|
+
const jobNumber = rawResult.JOB_NUMBER || rawResult.jobNumber;
|
|
304
|
+
let result = rawResult;
|
|
305
|
+
|
|
306
|
+
if ((jobStatus === 'scheduled' || jobStatus === 'accepted') && jobNumber) {
|
|
307
|
+
const fileCount = (data.files || []).length;
|
|
308
|
+
if (!jsonOutput) {
|
|
309
|
+
console.log(` Activating ${fileCount} file(s) as background job...`);
|
|
310
|
+
console.log('');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const endpoint = '/sap/bc/z_abapgit_agent/pull';
|
|
314
|
+
let finalJobResult;
|
|
315
|
+
try {
|
|
316
|
+
finalJobResult = await pollForCompletion(http, endpoint, jobNumber, {
|
|
317
|
+
pollInterval: 2000,
|
|
318
|
+
maxAttempts: 300,
|
|
319
|
+
onProgress: jsonOutput ? () => {} : (progress, message) => {
|
|
320
|
+
displayProgress(progress, message);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
} finally {
|
|
324
|
+
if (!jsonOutput) process.stdout.write('\n');
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (!finalJobResult || finalJobResult.status === 'error') {
|
|
328
|
+
const errMsg = (finalJobResult && (finalJobResult.result || finalJobResult.message)) || 'Pull background job failed';
|
|
329
|
+
const err = new Error(errMsg);
|
|
330
|
+
err._isPullError = true;
|
|
331
|
+
throw err;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Parse the result JSON string from the completed job
|
|
335
|
+
const resultStr = finalJobResult.result || finalJobResult.RESULT;
|
|
336
|
+
try {
|
|
337
|
+
result = typeof resultStr === 'string' ? JSON.parse(resultStr) : resultStr;
|
|
338
|
+
} catch (e) {
|
|
339
|
+
result = { success: '', message: 'Failed to parse pull job result' };
|
|
340
|
+
}
|
|
341
|
+
}
|
|
299
342
|
|
|
300
343
|
// Detect missing .abapgit.xml — without it abapGit's stored starting_folder
|
|
301
344
|
// may not match the actual source folder, causing pull to silently return ACTIVATED_COUNT=0
|
package/src/commands/unit.js
CHANGED
|
@@ -48,48 +48,84 @@ function buildRunConfigXml(adtUri, coverage) {
|
|
|
48
48
|
* Returns:
|
|
49
49
|
* {
|
|
50
50
|
* className,
|
|
51
|
-
* methods: [{
|
|
51
|
+
* methods: [{
|
|
52
|
+
* name, testClassName, passed, executionTime,
|
|
53
|
+
* kind?, title?, details?: string[], stack?: [{name, uri}]
|
|
54
|
+
* }],
|
|
52
55
|
* coverageStats: { totalLines, coveredLines, coverageRate } | null
|
|
53
56
|
* }
|
|
54
57
|
*/
|
|
55
58
|
function parseRunResult(xml, className) {
|
|
56
59
|
const methods = [];
|
|
57
60
|
|
|
58
|
-
// Split on <
|
|
59
|
-
const
|
|
60
|
-
for (let
|
|
61
|
-
const
|
|
61
|
+
// Split on <testClass blocks to capture test class name per method
|
|
62
|
+
const testClassBlocks = xml.split(/<testClass\s/);
|
|
63
|
+
for (let ci = 1; ci < testClassBlocks.length; ci++) {
|
|
64
|
+
const classBlock = testClassBlocks[ci];
|
|
62
65
|
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
const name = nameMatch[1];
|
|
66
|
+
const classNameMatch = classBlock.match(/adtcore:name="([^"]*)"/);
|
|
67
|
+
const testClassName = classNameMatch ? classNameMatch[1] : className;
|
|
66
68
|
|
|
67
|
-
//
|
|
68
|
-
const
|
|
69
|
-
|
|
69
|
+
// Split methods within this test class block
|
|
70
|
+
const methodBlocks = classBlock.split(/<testMethod\s/);
|
|
71
|
+
for (let i = 1; i < methodBlocks.length; i++) {
|
|
72
|
+
const block = methodBlocks[i];
|
|
70
73
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
const nameMatch = block.match(/adtcore:name="([^"]*)"/);
|
|
75
|
+
if (!nameMatch) continue;
|
|
76
|
+
const name = nameMatch[1];
|
|
77
|
+
|
|
78
|
+
const execTimeMatch = block.match(/executionTime="([^"]*)"/);
|
|
79
|
+
const executionTime = execTimeMatch ? parseFloat(execTimeMatch[1]) : 0;
|
|
75
80
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
81
|
+
// Find the <alerts> section
|
|
82
|
+
const alertsMatch = block.match(/<alerts>([\s\S]*?)<\/alerts>/);
|
|
83
|
+
const alertsContent = alertsMatch ? alertsMatch[1].trim() : '';
|
|
79
84
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
85
|
+
if (!alertsContent || alertsContent === '<alerts/>') {
|
|
86
|
+
methods.push({ name, testClassName, passed: true, executionTime });
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
83
89
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
90
|
+
// Extract alert kind
|
|
91
|
+
const kindMatch = block.match(/<alert\s[^>]*kind="([^"]*)"/);
|
|
92
|
+
const kind = kindMatch ? kindMatch[1] : 'failedAssertion';
|
|
93
|
+
|
|
94
|
+
// Extract title
|
|
95
|
+
const titleMatch = block.match(/<title[^>]*>([^<]*)<\/title>/);
|
|
96
|
+
const title = titleMatch ? titleMatch[1].trim() : 'Test failed';
|
|
97
|
+
|
|
98
|
+
// Extract all detail texts (may be multiple)
|
|
99
|
+
const details = [];
|
|
100
|
+
const detailRe = /<detail\s[^>]*text="([^"]*)"/g;
|
|
101
|
+
let dm;
|
|
102
|
+
while ((dm = detailRe.exec(block)) !== null) {
|
|
103
|
+
const t = dm[1].trim();
|
|
104
|
+
if (t) details.push(t);
|
|
105
|
+
}
|
|
87
106
|
|
|
88
|
-
|
|
107
|
+
// Extract stack entries: adtcore:name + adtcore:uri (contains #start=N for line)
|
|
108
|
+
const stack = [];
|
|
109
|
+
const stackRe = /<stackEntry\s([^>]*\/?>)/g;
|
|
110
|
+
let sm;
|
|
111
|
+
while ((sm = stackRe.exec(block)) !== null) {
|
|
112
|
+
const entry = sm[1];
|
|
113
|
+
const snMatch = entry.match(/adtcore:name="([^"]*)"/);
|
|
114
|
+
const suMatch = entry.match(/adtcore:uri="([^"]*)"/);
|
|
115
|
+
if (snMatch) {
|
|
116
|
+
const lineMatch = suMatch && suMatch[1].match(/#start=(\d+)/);
|
|
117
|
+
stack.push({
|
|
118
|
+
name: snMatch[1],
|
|
119
|
+
line: lineMatch ? parseInt(lineMatch[1], 10) : null,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
methods.push({ name, testClassName, passed: false, executionTime, kind, title, details, stack });
|
|
125
|
+
}
|
|
89
126
|
}
|
|
90
127
|
|
|
91
128
|
// Parse coverage stats if present
|
|
92
|
-
// <coverage ... adtcore:lines_total="N" adtcore:lines_covered="M" adtcore:coverage_rate="R"/>
|
|
93
129
|
let coverageStats = null;
|
|
94
130
|
const covMatch = xml.match(/<coverage\b[^>]*>/);
|
|
95
131
|
if (covMatch) {
|
|
@@ -122,6 +158,9 @@ function buildUnitJUnit(results) {
|
|
|
122
158
|
const failedMethods = methods.filter(m => !m.passed);
|
|
123
159
|
const syntheticFailures = thresholdFailure ? 1 : 0;
|
|
124
160
|
const totalFailures = failedMethods.length + syntheticFailures;
|
|
161
|
+
// classname "ZCL_MY_TEST.LTCL_UNIT_TEST" groups under the same ABAP class node
|
|
162
|
+
// as "ZCL_MY_TEST.Inspect" from the inspect report — one node per ABAP class.
|
|
163
|
+
const suiteLabel = className;
|
|
125
164
|
|
|
126
165
|
const lines = [];
|
|
127
166
|
|
|
@@ -134,16 +173,21 @@ function buildUnitJUnit(results) {
|
|
|
134
173
|
}
|
|
135
174
|
|
|
136
175
|
if (testCount === 0) {
|
|
137
|
-
lines.push(` <testcase name="(no tests)" classname="${escapeXml(
|
|
176
|
+
lines.push(` <testcase name="(no tests)" classname="${escapeXml(suiteLabel)}"/>`);
|
|
138
177
|
} else {
|
|
139
178
|
for (const m of methods) {
|
|
179
|
+
const timeAttr = m.executionTime != null ? ` time="${m.executionTime}"` : '';
|
|
180
|
+
// "Unit Tests / ZCL_MY_TEST.LTCL_UNIT_TEST" groups by namespace → class → local test class.
|
|
181
|
+
const testClass = m.testClassName ? `${suiteLabel}.${m.testClassName}` : suiteLabel;
|
|
140
182
|
if (m.passed) {
|
|
141
|
-
lines.push(` <testcase name="${escapeXml(m.name)}" classname="${escapeXml(
|
|
183
|
+
lines.push(` <testcase name="${escapeXml(m.name)}" classname="${escapeXml(testClass)}"${timeAttr}/>`);
|
|
142
184
|
} else {
|
|
143
185
|
const msg = escapeXml(m.title || 'Test failed');
|
|
144
|
-
const
|
|
186
|
+
const bodyParts = [m.title || 'Test failed'];
|
|
187
|
+
if (m.details && m.details.length) bodyParts.push(...m.details);
|
|
188
|
+
const body = escapeXml(bodyParts.join('\n'));
|
|
145
189
|
lines.push(
|
|
146
|
-
` <testcase name="${escapeXml(m.name)}" classname="${escapeXml(
|
|
190
|
+
` <testcase name="${escapeXml(m.name)}" classname="${escapeXml(testClass)}"${timeAttr}>\n` +
|
|
147
191
|
` <failure type="${escapeXml(m.kind || 'failedAssertion')}" message="${msg}">${body}</failure>\n` +
|
|
148
192
|
` </testcase>`
|
|
149
193
|
);
|
|
@@ -153,14 +197,14 @@ function buildUnitJUnit(results) {
|
|
|
153
197
|
|
|
154
198
|
if (thresholdFailure) {
|
|
155
199
|
lines.push(
|
|
156
|
-
` <testcase name="coverage_threshold" classname="${escapeXml(
|
|
200
|
+
` <testcase name="coverage_threshold" classname="${escapeXml(suiteLabel)}">\n` +
|
|
157
201
|
` <failure type="FAILURE" message="${escapeXml(thresholdFailure)}">${escapeXml(thresholdFailure)}</failure>\n` +
|
|
158
202
|
` </testcase>`
|
|
159
203
|
);
|
|
160
204
|
}
|
|
161
205
|
|
|
162
206
|
return (
|
|
163
|
-
` <testsuite name="${escapeXml(
|
|
207
|
+
` <testsuite name="${escapeXml(suiteLabel)}" ` +
|
|
164
208
|
`tests="${Math.max(testCount + syntheticFailures, 1)}" failures="${totalFailures}" errors="0">\n` +
|
|
165
209
|
lines.join('\n') + '\n' +
|
|
166
210
|
` </testsuite>`
|
|
@@ -231,10 +275,42 @@ async function runUnitTestForFile(sourceFile, adt, coverage, jsonOutput, verbose
|
|
|
231
275
|
console.log(` Covered Lines: ${coveredLines}`);
|
|
232
276
|
}
|
|
233
277
|
|
|
278
|
+
// Slow test summary: show methods with executionTime > 0.1s
|
|
279
|
+
const slowThreshold = 0.1;
|
|
280
|
+
const slowMethods = result.methods
|
|
281
|
+
.filter(m => m.executionTime > slowThreshold)
|
|
282
|
+
.sort((a, b) => b.executionTime - a.executionTime);
|
|
283
|
+
if (slowMethods.length > 0) {
|
|
284
|
+
const slowList = slowMethods.map(m => `${m.name} (${m.executionTime.toFixed(3)}s)`).join(', ');
|
|
285
|
+
console.log(` ⏱ Slowest: ${slowList}`);
|
|
286
|
+
}
|
|
287
|
+
|
|
234
288
|
if (failedCount > 0) {
|
|
235
289
|
for (const m of result.methods.filter(m => !m.passed)) {
|
|
236
|
-
|
|
290
|
+
const prefix = m.testClassName || className;
|
|
291
|
+
console.log(`\n ✗ ${prefix}=>${m.name}`);
|
|
292
|
+
console.log(` ${m.title || 'Test failed'}`);
|
|
293
|
+
if (m.details && m.details.length) {
|
|
294
|
+
for (const d of m.details) console.log(` ${d}`);
|
|
295
|
+
}
|
|
296
|
+
if (m.stack && m.stack.length) {
|
|
297
|
+
for (const s of m.stack) {
|
|
298
|
+
const loc = s.line != null ? ` (line ${s.line})` : '';
|
|
299
|
+
console.log(` at ${s.name}${loc}`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
237
302
|
}
|
|
303
|
+
console.log('');
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// --verbose: per-method time table
|
|
307
|
+
if (verbose && testCount > 0) {
|
|
308
|
+
console.log(` Method times:`);
|
|
309
|
+
for (const m of result.methods) {
|
|
310
|
+
const mark = m.passed ? '✓' : '✗';
|
|
311
|
+
console.log(` ${mark} ${m.name.padEnd(40)} ${(m.executionTime || 0).toFixed(3)}s`);
|
|
312
|
+
}
|
|
313
|
+
console.log('');
|
|
238
314
|
}
|
|
239
315
|
}
|
|
240
316
|
|
|
@@ -285,6 +361,7 @@ Parameters:
|
|
|
285
361
|
--coverage-mode <warn|fail> Action when below threshold: warn = UNSTABLE, fail = error. Default: fail.
|
|
286
362
|
--junit-output <file> Write results as JUnit XML to this file.
|
|
287
363
|
--json Output as JSON.
|
|
364
|
+
--verbose Show per-method execution times; also print raw HTTP error responses.
|
|
288
365
|
|
|
289
366
|
Examples:
|
|
290
367
|
abapgit-agent unit --files src/zcl_my_test.clas.testclasses.abap
|
|
@@ -368,6 +445,24 @@ Examples:
|
|
|
368
445
|
}
|
|
369
446
|
}
|
|
370
447
|
|
|
448
|
+
// Failed tests summary
|
|
449
|
+
if (!jsonOutput) {
|
|
450
|
+
const allFailed = results.flatMap(r => (r.methods || []).filter(m => !m.passed).map(m => ({ ...m, className: r.className })));
|
|
451
|
+
if (allFailed.length > 0) {
|
|
452
|
+
console.log('\nFailed Tests:');
|
|
453
|
+
console.log('─'.repeat(80));
|
|
454
|
+
for (const m of allFailed) {
|
|
455
|
+
const prefix = m.testClassName || m.className;
|
|
456
|
+
console.log(` ✗ ${prefix}=>${m.name}`);
|
|
457
|
+
console.log(` ${m.title || 'Test failed'}`);
|
|
458
|
+
if (m.details && m.details.length) {
|
|
459
|
+
for (const d of m.details) console.log(` ${d}`);
|
|
460
|
+
}
|
|
461
|
+
console.log('');
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
371
466
|
// JUnit output
|
|
372
467
|
if (junitOutput) {
|
|
373
468
|
const xml = buildUnitJUnit(results);
|
package/src/commands/view.js
CHANGED
|
@@ -246,7 +246,7 @@ Description:
|
|
|
246
246
|
Parameters:
|
|
247
247
|
--objects <obj1,...> Comma-separated object names (required).
|
|
248
248
|
--type <type> Object type: CLAS, INTF, PROG, TABL, STRU, DTEL, TTYP, DOMA,
|
|
249
|
-
DDLS, DCLS, MSAG, FUGR (auto-detected from TADIR if omitted).
|
|
249
|
+
DDLS, DCLS, MSAG, FUGR, SUSO (auto-detected from TADIR if omitted).
|
|
250
250
|
--full Show full source including all method implementations.
|
|
251
251
|
--lines Show dual line numbers (G = global for debug set, [N] = include-local).
|
|
252
252
|
--fm <name> With --full: show only the specified function module (FUGR only).
|
|
@@ -593,6 +593,51 @@ Examples:
|
|
|
593
593
|
console.log(` ${desc}`);
|
|
594
594
|
}
|
|
595
595
|
}
|
|
596
|
+
} else if (objType === 'SUSO' || objType === 'Authorization Object') {
|
|
597
|
+
// SUSO: property box for object class + fields, then activities list
|
|
598
|
+
const classComp = components.filter(c => String(c.FIELD || c.field || '').startsWith('OCLSS'));
|
|
599
|
+
const fieldComps = components.filter(c => String(c.FIELD || c.field || '').startsWith('FIELD_'));
|
|
600
|
+
const actvtComps = components.filter(c => String(c.FIELD || c.field || '').startsWith('ACTVT_'));
|
|
601
|
+
|
|
602
|
+
const propWidth = 16;
|
|
603
|
+
const valueWidth = 42;
|
|
604
|
+
const sep = '┌' + '─'.repeat(propWidth + 2) + '┬' + '─'.repeat(valueWidth + 2) + '┐';
|
|
605
|
+
const mid = '├' + '─'.repeat(propWidth + 2) + '┼' + '─'.repeat(valueWidth + 2) + '┤';
|
|
606
|
+
const end = '└' + '─'.repeat(propWidth + 2) + '┴' + '─'.repeat(valueWidth + 2) + '┘';
|
|
607
|
+
const buildPropRow = (property, value) =>
|
|
608
|
+
'│ ' + String(property || '').padEnd(propWidth) + ' │ ' +
|
|
609
|
+
String(value || '').substring(0, valueWidth).padEnd(valueWidth) + ' │';
|
|
610
|
+
|
|
611
|
+
console.log(` AUTH OBJECT ${objName}:`);
|
|
612
|
+
console.log(sep);
|
|
613
|
+
console.log(buildPropRow('Property', 'Value'));
|
|
614
|
+
console.log(mid);
|
|
615
|
+
|
|
616
|
+
for (const comp of classComp) {
|
|
617
|
+
const desc = comp.DESCRIPTION || comp.description || '';
|
|
618
|
+
const colonIdx = desc.indexOf(': ');
|
|
619
|
+
const value = colonIdx !== -1 ? desc.substring(colonIdx + 2) : desc;
|
|
620
|
+
console.log(buildPropRow('Object class', value));
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
for (const comp of fieldComps) {
|
|
624
|
+
const desc = comp.DESCRIPTION || comp.description || '';
|
|
625
|
+
const colonIdx = desc.indexOf(': ');
|
|
626
|
+
const value = colonIdx !== -1 ? desc.substring(colonIdx + 2) : desc;
|
|
627
|
+
const idx = String(comp.FIELD || comp.field || '').replace('FIELD_', '');
|
|
628
|
+
console.log(buildPropRow(`Field ${idx}`, value));
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
console.log(end);
|
|
632
|
+
|
|
633
|
+
if (actvtComps.length > 0) {
|
|
634
|
+
console.log('');
|
|
635
|
+
console.log(` Allowed activities (${actvtComps.length}):`);
|
|
636
|
+
for (const comp of actvtComps) {
|
|
637
|
+
const desc = comp.DESCRIPTION || comp.description || '';
|
|
638
|
+
console.log(` ${desc}`);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
596
641
|
} else {
|
|
597
642
|
// Build table display for TABL/STRU with Data Element and Description
|
|
598
643
|
const colWidths = {
|