abapgit-agent 1.6.0 → 1.6.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/00_index.md +1 -0
- package/abap/guidelines/01_sql.md +16 -0
- package/abap/guidelines/02_exceptions.md +68 -0
- package/abap/guidelines/03_testing.md +17 -0
- package/abap/guidelines/04_cds.md +16 -0
- package/abap/guidelines/05_classes.md +8 -0
- package/abap/guidelines/06_objects.md +7 -0
- package/abap/guidelines/07_json.md +2 -0
- package/abap/guidelines/08_abapgit.md +29 -0
- package/abap/guidelines/09_unit_testable_code.md +568 -0
- package/bin/abapgit-agent +68 -30
- package/package.json +1 -1
- package/src/config.js +1 -1
- package/src/ref-search.js +92 -44
|
@@ -14,6 +14,7 @@ This folder contains detailed ABAP coding guidelines that can be searched using
|
|
|
14
14
|
| `06_objects.md` | Object Naming Conventions |
|
|
15
15
|
| `07_json.md` | JSON Handling |
|
|
16
16
|
| `08_abapgit.md` | abapGit XML Metadata Templates |
|
|
17
|
+
| `09_unit_testable_code.md` | Unit Testable Code Guidelines (Dependency Injection) |
|
|
17
18
|
|
|
18
19
|
## Usage
|
|
19
20
|
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
# ABAP SQL Best Practices
|
|
2
2
|
|
|
3
|
+
**Searchable keywords**: SELECT, FROM, WHERE, ABAP SQL, Open SQL, host variable, @ prefix, range table, INTO, UP TO, OFFSET, GROUP BY, JOIN
|
|
4
|
+
|
|
3
5
|
When writing ABAP SQL (Open SQL) queries, follow these rules:
|
|
4
6
|
|
|
7
|
+
## TOPICS IN THIS FILE
|
|
8
|
+
1. Host Variables (@ prefix) - line 5
|
|
9
|
+
2. Range Tables (IN clause) - line 17
|
|
10
|
+
3. SELECT Clause Order - line 35
|
|
11
|
+
4. Fixed Point Arithmetic - line 52
|
|
12
|
+
5. Field Separation - line 62
|
|
13
|
+
|
|
5
14
|
## 1. Host Variables - Use @ Prefix
|
|
6
15
|
|
|
7
16
|
Use `@` prefix for host variables in ABAP SQL:
|
|
@@ -70,3 +79,10 @@ SELECT object, obj_name FROM tadir ...
|
|
|
70
79
|
" Wrong - missing comma
|
|
71
80
|
SELECT object obj_name FROM tadir ...
|
|
72
81
|
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## See Also
|
|
86
|
+
- **Constructor Expressions** (05_classes.md) - for VALUE #(), FILTER, FOR loops
|
|
87
|
+
- **Internal Tables** - for filtering and iteration patterns
|
|
88
|
+
- **abapGit** (08_abapgit.md) - for XML metadata templates
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Exception Handling - Classical vs Class-Based
|
|
2
2
|
|
|
3
|
+
**Searchable keywords**: exception, RAISING, TRY, CATCH, cx_static_check, cx_dynamic_check, EXCEPTIONS, sy-subrc, class-based exception, classical exception
|
|
4
|
+
|
|
5
|
+
## TOPICS IN THIS FILE
|
|
6
|
+
1. Quick Identification - line 5
|
|
7
|
+
2. Classical Exceptions - line 12
|
|
8
|
+
3. Class-Based Exceptions - line 25
|
|
9
|
+
4. Method Signatures - line 50
|
|
10
|
+
5. Best Practices - line 80
|
|
11
|
+
6. Interface vs Class Methods - line 118
|
|
12
|
+
|
|
3
13
|
ABAP has two exception handling mechanisms. Using the wrong one causes silent failures.
|
|
4
14
|
|
|
5
15
|
## Quick Identification
|
|
@@ -106,3 +116,61 @@ Or search the cheat sheets:
|
|
|
106
116
|
```bash
|
|
107
117
|
abapgit-agent ref --topic exceptions
|
|
108
118
|
```
|
|
119
|
+
|
|
120
|
+
## Interface vs Class Methods
|
|
121
|
+
|
|
122
|
+
### Interface Methods
|
|
123
|
+
|
|
124
|
+
Cannot add RAISING clause in the implementing class. Options:
|
|
125
|
+
|
|
126
|
+
1. Add RAISING to interface definition
|
|
127
|
+
2. Use TRY-CATCH in the implementation
|
|
128
|
+
|
|
129
|
+
```abap
|
|
130
|
+
" Interface definition - can add RAISING here
|
|
131
|
+
INTERFACE zif_example.
|
|
132
|
+
methods execute
|
|
133
|
+
importing is_param type data optional
|
|
134
|
+
returning value(rv_result) type string
|
|
135
|
+
raising cx_static_check.
|
|
136
|
+
ENDINTERFACE.
|
|
137
|
+
|
|
138
|
+
" Implementation - CANNOT add RAISING here
|
|
139
|
+
CLASS zcl_example DEFINITION.
|
|
140
|
+
INTERFACES zif_example.
|
|
141
|
+
ENDCLASS.
|
|
142
|
+
|
|
143
|
+
CLASS zcl_example IMPLEMENTATION.
|
|
144
|
+
METHOD zif_example~execute.
|
|
145
|
+
" Must handle cx_static_check here with TRY-CATCH
|
|
146
|
+
" or declare it in interface, not here
|
|
147
|
+
ENDMETHOD.
|
|
148
|
+
ENDCLASS.
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Class Methods
|
|
152
|
+
|
|
153
|
+
Can add RAISING clause to declare exceptions, allowing caller to handle in one place:
|
|
154
|
+
|
|
155
|
+
```abap
|
|
156
|
+
" Class method with RAISING clause
|
|
157
|
+
METHODS process_data
|
|
158
|
+
importing iv_data type string
|
|
159
|
+
returning value(rv_result) type string
|
|
160
|
+
raising cx_static_check.
|
|
161
|
+
|
|
162
|
+
" Caller can handle in one place
|
|
163
|
+
TRY.
|
|
164
|
+
lv_result = lo_obj->process_data( iv_data = 'test' ).
|
|
165
|
+
CATCH cx_static_check.
|
|
166
|
+
" Handle exception
|
|
167
|
+
ENDTRY.
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### When to Use Each
|
|
171
|
+
|
|
172
|
+
| Scenario | Recommendation |
|
|
173
|
+
|----------|----------------|
|
|
174
|
+
| Multiple callers need to handle exception | Add RAISING to method definition |
|
|
175
|
+
| Exception should be handled internally | Use TRY-CATCH in implementation |
|
|
176
|
+
| Interface method | Add RAISING to interface, or use TRY-CATCH in class |
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Unit Testing
|
|
2
2
|
|
|
3
|
+
**Searchable keywords**: unit test, AUnit, test class, cl_abap_unit_assert, FOR TESTING, setup, teardown, RISK LEVEL, DURATION, CDS test double, CL_CDS_TEST_ENVIRONMENT
|
|
4
|
+
|
|
5
|
+
## TOPICS IN THIS FILE
|
|
6
|
+
1. Local Test Classes - line 3
|
|
7
|
+
2. File Structure - line 5
|
|
8
|
+
3. Required Elements - line 16
|
|
9
|
+
4. Naming Conventions - line 48
|
|
10
|
+
5. CDS Test Doubles - line 94
|
|
11
|
+
6. CDS with Aggregations - line 178
|
|
12
|
+
|
|
3
13
|
## Unit Testing with Local Test Classes
|
|
4
14
|
|
|
5
15
|
### File Structure
|
|
@@ -250,3 +260,10 @@ ENDMETHOD.
|
|
|
250
260
|
abapgit-agent ref "cl_cds_test_environment"
|
|
251
261
|
abapgit-agent ref --topic unit-tests
|
|
252
262
|
```
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## See Also
|
|
267
|
+
- **CDS Views** (04_cds.md) - for CDS view definitions and syntax
|
|
268
|
+
- **abapGit** (08_abapgit.md) - for WITH_UNIT_TESTS in XML metadata
|
|
269
|
+
- **ABAP SQL** (01_sql.md) - for SELECT statements in tests
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Creating CDS Views
|
|
2
2
|
|
|
3
|
+
**Searchable keywords**: CDS, DDL, DDLS, CDS view, @AbapCatalog, @AccessControl, association, projection, consumption
|
|
4
|
+
|
|
5
|
+
## TOPICS IN THIS FILE
|
|
6
|
+
1. File Naming - line 7
|
|
7
|
+
2. DDL Source (.ddls.asddls) - line 18
|
|
8
|
+
3. Annotations - line 50
|
|
9
|
+
4. Associations - line 75
|
|
10
|
+
5. CDS Test Doubles - see 03_testing.md
|
|
11
|
+
|
|
3
12
|
## Creating CDS Views (DDLS)
|
|
4
13
|
|
|
5
14
|
CDS views (Data Definition Language Source) require specific file naming and structure for abapGit.
|
|
@@ -118,3 +127,10 @@ When working with CDS view syntax (arithmetic, built-in functions, aggregations,
|
|
|
118
127
|
- `zdemo_abap_cds_ve_assoc.ddls.asddls` - Associations
|
|
119
128
|
|
|
120
129
|
**Note**: This requires `abap-cheat-sheets` to be in the reference folder (configured in `.abapGitAgent`).
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## See Also
|
|
134
|
+
- **Unit Testing** (03_testing.md) - for CDS Test Double Framework
|
|
135
|
+
- **abapGit** (08_abapgit.md) - for CDS XML metadata templates
|
|
136
|
+
- **ABAP SQL** (01_sql.md) - for SQL functions used in CDS
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# ABAP Classes and Objects
|
|
2
2
|
|
|
3
|
+
**Searchable keywords**: CLASS, DEFINITION, PUBLIC, CREATE OBJECT, NEW, METHOD, INTERFACES, inheritance, FINAL, ABSTRACT
|
|
4
|
+
|
|
5
|
+
## TOPICS IN THIS FILE
|
|
6
|
+
1. Class Definition (PUBLIC) - line 3
|
|
7
|
+
2. Constructor - line 20
|
|
8
|
+
3. Interfaces - line 35
|
|
9
|
+
4. Inline Declaration - line 50
|
|
10
|
+
|
|
3
11
|
## ABAP Class Definition - Must Use PUBLIC
|
|
4
12
|
|
|
5
13
|
**CRITICAL**: Global ABAP classes MUST use `PUBLIC` in the class definition:
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# ABAP Objects
|
|
2
2
|
|
|
3
|
+
**Searchable keywords**: naming convention, Z prefix, namespace, object type, CLAS, INTF, PROG, TABL, DDLS, XML metadata, .abapGit.xml
|
|
4
|
+
|
|
5
|
+
## TOPICS IN THIS FILE
|
|
6
|
+
1. XML Metadata Required - line 3
|
|
7
|
+
2. Naming Conventions - line 30
|
|
8
|
+
3. Object Types - line 60
|
|
9
|
+
|
|
3
10
|
## Creating New ABAP Objects - XML Metadata Required
|
|
4
11
|
|
|
5
12
|
**CRITICAL CHECKLIST - Never Forget!**
|
|
@@ -2,6 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
Each ABAP object requires an XML metadata file for abapGit to understand how to serialize/deserialize it. This guide provides templates for common object types.
|
|
4
4
|
|
|
5
|
+
## QUICK REFERENCE
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
File Type | ABAP File | XML File
|
|
9
|
+
-------------------|------------------------------|-------------------
|
|
10
|
+
Class | zcl_*.clas.abap | zcl_*.clas.xml
|
|
11
|
+
Test Class | zcl_*.clas.testclasses.abap | zcl_*.clas.xml
|
|
12
|
+
Interface | zif_*.intf.abap | zif_*.intf.xml
|
|
13
|
+
Program | z*.prog.abap | z*.prog.xml
|
|
14
|
+
Table | z*.tabl.abap | z*.tabl.xml
|
|
15
|
+
CDS View | zc_*.ddls.asddls | zc_*.ddls.xml
|
|
16
|
+
CDS Entity | ze_*.ddlx.asddlx | ze_*.ddlx.xml
|
|
17
|
+
Data Element | z*.dtel.abap | z*.dtel.xml
|
|
18
|
+
Structure | z*.stru.abap | z*.stru.xml
|
|
19
|
+
Table Type | z*.ttyp.abap | z*.ttyp.xml
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
Key XML Settings:
|
|
24
|
+
Class EXPOSURE: 2=Public, 3=Protected, 4=Private
|
|
25
|
+
Class STATE: 1=Active
|
|
26
|
+
Table TABCLASS: TRANSP, POOL, CLUSTER
|
|
27
|
+
Table DELIVERY: A=Application, C=Customizing
|
|
28
|
+
CDS SOURCE_TYPE: V=View, C=Consumption
|
|
29
|
+
Test Class XML: <WITH_UNIT_TESTS>X</WITH_UNIT_TESTS>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**Searchable keywords**: class xml, interface xml, table xml, cds xml, test class, exposure, serializer, abapgit
|
|
33
|
+
|
|
5
34
|
## Why XML Metadata?
|
|
6
35
|
|
|
7
36
|
abapGit needs XML files to:
|
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
# ABAP Unit Testable Code Guidelines
|
|
2
|
+
|
|
3
|
+
This document provides guidelines for creating ABAP OO classes/interfaces that can be easily unit tested with test doubles. These guidelines help AI coding tools understand how to design classes that are testable without requiring real dependencies.
|
|
4
|
+
|
|
5
|
+
## The Problem
|
|
6
|
+
|
|
7
|
+
When ABAP classes are not designed for testability, unit tests cannot mock dependencies. This leads to:
|
|
8
|
+
|
|
9
|
+
- Tests that depend on real external systems (databases, APIs, file systems)
|
|
10
|
+
- Tests that fail in different environments
|
|
11
|
+
- Tests that are slow and unreliable
|
|
12
|
+
- Impossible to test error conditions
|
|
13
|
+
|
|
14
|
+
**Example of untestable code:**
|
|
15
|
+
|
|
16
|
+
```abap
|
|
17
|
+
" BAD - Hardcoded dependency, cannot be replaced in tests
|
|
18
|
+
CLASS zcl_abgagt_command_pull DEFINITION PUBLIC.
|
|
19
|
+
METHOD execute.
|
|
20
|
+
lo_agent = NEW zcl_abgagt_agent( ). " Hardcoded!
|
|
21
|
+
ls_result = lo_agent->pull( ... ). " Calls real system
|
|
22
|
+
ENDMETHOD.
|
|
23
|
+
ENDCLASS.
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The unit test will instantiate the REAL `zcl_abgagt_agent` which tries to connect to abapGit and a real git repository, causing test failures.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Core Principles
|
|
31
|
+
|
|
32
|
+
### 1. Dependency Inversion (Dependency Injection)
|
|
33
|
+
|
|
34
|
+
**Pass dependencies through constructor instead of creating them internally.**
|
|
35
|
+
|
|
36
|
+
```abap
|
|
37
|
+
" GOOD - Dependency injected via constructor
|
|
38
|
+
CLASS zcl_abgagt_command_pull DEFINITION PUBLIC.
|
|
39
|
+
PUBLIC SECTION.
|
|
40
|
+
INTERFACES zif_abgagt_command.
|
|
41
|
+
|
|
42
|
+
" Constructor injection
|
|
43
|
+
METHODS constructor
|
|
44
|
+
IMPORTING
|
|
45
|
+
io_agent TYPE REF TO zif_abgagt_agent.
|
|
46
|
+
|
|
47
|
+
PRIVATE SECTION.
|
|
48
|
+
DATA mo_agent TYPE REF TO zif_abgagt_agent.
|
|
49
|
+
|
|
50
|
+
ENDCLASS.
|
|
51
|
+
|
|
52
|
+
CLASS zcl_abgagt_command_pull IMPLEMENTATION.
|
|
53
|
+
|
|
54
|
+
METHOD constructor.
|
|
55
|
+
super->constructor( ).
|
|
56
|
+
mo_agent = io_agent.
|
|
57
|
+
ENDMETHOD.
|
|
58
|
+
|
|
59
|
+
METHOD execute.
|
|
60
|
+
" Use injected dependency
|
|
61
|
+
ls_result = mo_agent->pull( ... ).
|
|
62
|
+
ENDMETHOD.
|
|
63
|
+
|
|
64
|
+
ENDCLASS.
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**In production code:**
|
|
68
|
+
```abap
|
|
69
|
+
DATA(lo_command) = NEW zcl_abgagt_command_pull(
|
|
70
|
+
io_agent = NEW zcl_abgagt_agent( ) ).
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**In test code:**
|
|
74
|
+
```abap
|
|
75
|
+
" Create test double
|
|
76
|
+
CLASS ltd_mock_agent DEFINITION FOR TESTING.
|
|
77
|
+
PUBLIC SECTION.
|
|
78
|
+
INTERFACES zif_abgagt_agent PARTIALLY IMPLEMENTED.
|
|
79
|
+
ENDCLASS.
|
|
80
|
+
|
|
81
|
+
CLASS ltd_mock_agent IMPLEMENTATION.
|
|
82
|
+
METHOD zif_abgagt_agent~pull.
|
|
83
|
+
" Return test data instead of calling real system
|
|
84
|
+
rs_result-success = abap_true.
|
|
85
|
+
rs_result-message = 'Test success'.
|
|
86
|
+
ENDMETHOD.
|
|
87
|
+
ENDCLASS.
|
|
88
|
+
|
|
89
|
+
" Test uses test double
|
|
90
|
+
CLASS ltcl_test DEFINITION FOR TESTING.
|
|
91
|
+
METHOD test_execute.
|
|
92
|
+
DATA(lo_mock) = NEW ltd_mock_agent( ).
|
|
93
|
+
DATA(lo_cut) = NEW zcl_abgagt_command_pull( io_agent = lo_mock ).
|
|
94
|
+
|
|
95
|
+
DATA(lv_result) = lo_cut->execute( ... ).
|
|
96
|
+
|
|
97
|
+
" Assert expected results
|
|
98
|
+
ENDMETHOD.
|
|
99
|
+
ENDCLASS.
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### 2. Always Use Interfaces for Dependencies
|
|
103
|
+
|
|
104
|
+
**Never depend on concrete classes - depend on interfaces.**
|
|
105
|
+
|
|
106
|
+
```abap
|
|
107
|
+
" GOOD - Depend on interface
|
|
108
|
+
DATA mo_agent TYPE REF TO zif_abgagt_agent. " Interface!
|
|
109
|
+
|
|
110
|
+
" BAD - Depends on concrete class
|
|
111
|
+
DATA mo_agent TYPE REF TO zcl_abgagt_agent. " Concrete class!
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
This allows you to replace the implementation with test doubles.
|
|
115
|
+
|
|
116
|
+
### 3. Make Dependencies Injectable via Constructor
|
|
117
|
+
|
|
118
|
+
**Use constructor injection, not setter injection.**
|
|
119
|
+
|
|
120
|
+
```abap
|
|
121
|
+
" GOOD - Constructor injection (required dependency)
|
|
122
|
+
METHODS constructor
|
|
123
|
+
IMPORTING
|
|
124
|
+
io_agent TYPE REF TO zif_abgagt_agent.
|
|
125
|
+
|
|
126
|
+
" BAD - Setter injection (optional, can be forgotten)
|
|
127
|
+
METHODS set_agent
|
|
128
|
+
IMPORTING
|
|
129
|
+
io_agent TYPE REF TO zif_abgagt_agent.
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Constructor injection:
|
|
133
|
+
- Makes dependency explicit
|
|
134
|
+
- Ensures object is always in valid state
|
|
135
|
+
- Cannot forget to inject
|
|
136
|
+
|
|
137
|
+
### 4. Avoid Static Calls
|
|
138
|
+
|
|
139
|
+
**Static method calls cannot be mocked/test-doubled.**
|
|
140
|
+
|
|
141
|
+
```abap
|
|
142
|
+
" BAD - Static call cannot be replaced
|
|
143
|
+
DATA(li_repo) = zcl_abapgit_repo_srv=>get_instance( )->get_repo_from_url( ... ).
|
|
144
|
+
|
|
145
|
+
" GOOD - Instance method via injected dependency
|
|
146
|
+
DATA(li_repo) = mo_repo_srv->get_repo_from_url( ... ).
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
If you must call static methods, wrap them in an instance method of an injected class.
|
|
150
|
+
|
|
151
|
+
### 5. Keep Constructor Simple
|
|
152
|
+
|
|
153
|
+
**Constructor should only assign dependencies, not perform complex logic.**
|
|
154
|
+
|
|
155
|
+
```abap
|
|
156
|
+
" GOOD - Simple constructor
|
|
157
|
+
METHOD constructor.
|
|
158
|
+
mo_agent = io_agent.
|
|
159
|
+
mo_logger = io_logger.
|
|
160
|
+
ENDMETHOD.
|
|
161
|
+
|
|
162
|
+
" BAD - Complex logic in constructor
|
|
163
|
+
METHOD constructor.
|
|
164
|
+
mo_agent = io_agent.
|
|
165
|
+
" Don't do this here:
|
|
166
|
+
mo_agent->connect( ). " Network call in constructor!
|
|
167
|
+
DATA(ls_config) = read_config( ). " File I/O in constructor!
|
|
168
|
+
ENDMETHOD.
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Injection Techniques
|
|
174
|
+
|
|
175
|
+
### Constructor Injection (Recommended)
|
|
176
|
+
|
|
177
|
+
```abap
|
|
178
|
+
CLASS zcl_my_class DEFINITION PUBLIC.
|
|
179
|
+
PUBLIC SECTION.
|
|
180
|
+
METHODS constructor
|
|
181
|
+
IMPORTING
|
|
182
|
+
io_dependency TYPE REF TO zif_my_interface.
|
|
183
|
+
PRIVATE SECTION.
|
|
184
|
+
DATA mo_dependency TYPE REF TO zif_my_interface.
|
|
185
|
+
ENDCLASS.
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Back Door Injection (for existing code)
|
|
189
|
+
|
|
190
|
+
When you cannot modify the constructor, use friendship:
|
|
191
|
+
|
|
192
|
+
```abap
|
|
193
|
+
" In test class
|
|
194
|
+
CLASS zcl_my_class DEFINITION LOCAL FRIENDS ltcl_test.
|
|
195
|
+
|
|
196
|
+
CLASS ltcl_test IMPLEMENTATION.
|
|
197
|
+
METHOD test_with_mock.
|
|
198
|
+
" Directly set private attribute via friendship
|
|
199
|
+
CREATE OBJECT mo_cut.
|
|
200
|
+
mo_cut->mo_dependency = lo_mock. " Access private attribute
|
|
201
|
+
ENDMETHOD.
|
|
202
|
+
ENDCLASS.
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Test Seams (last resort)
|
|
206
|
+
|
|
207
|
+
For legacy code that cannot be refactored:
|
|
208
|
+
|
|
209
|
+
```abap
|
|
210
|
+
" In production code
|
|
211
|
+
METHOD get_data.
|
|
212
|
+
TEST-SEAM db_select.
|
|
213
|
+
SELECT * FROM dbtab INTO TABLE @DATA(lt_data).
|
|
214
|
+
END-TEST-SEAM.
|
|
215
|
+
ENDMETHOD.
|
|
216
|
+
|
|
217
|
+
" In test class
|
|
218
|
+
METHOD test_get_data.
|
|
219
|
+
TEST-INJECTION db_select.
|
|
220
|
+
lt_data = VALUE #( ( id = '1' ) ( id = '2' ) ).
|
|
221
|
+
END-TEST-INJECTION.
|
|
222
|
+
|
|
223
|
+
DATA(lt_result) = mo_cut->get_data( ).
|
|
224
|
+
ENDMETHOD.
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Test Double Patterns
|
|
230
|
+
|
|
231
|
+
### Manual Test Double (Local Class)
|
|
232
|
+
|
|
233
|
+
```abap
|
|
234
|
+
" Create test double class
|
|
235
|
+
CLASS ltd_mock_reader DEFINITION FOR TESTING.
|
|
236
|
+
PUBLIC SECTION.
|
|
237
|
+
INTERFACES zif_data_reader PARTIALLY IMPLEMENTED.
|
|
238
|
+
METHODS set_result_data
|
|
239
|
+
IMPORTING it_data TYPE ANY TABLE.
|
|
240
|
+
PRIVATE SECTION.
|
|
241
|
+
DATA mt_data TYPE ANY TABLE.
|
|
242
|
+
ENDCLASS.
|
|
243
|
+
|
|
244
|
+
CLASS ltd_mock_reader IMPLEMENTATION.
|
|
245
|
+
METHOD set_result_data.
|
|
246
|
+
mt_data = it_data.
|
|
247
|
+
ENDMETHOD.
|
|
248
|
+
|
|
249
|
+
METHOD zif_data_reader~read_all.
|
|
250
|
+
rt_data = mt_data.
|
|
251
|
+
ENDMETHOD.
|
|
252
|
+
ENDCLASS.
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Using ABAP Test Double Framework
|
|
256
|
+
|
|
257
|
+
```abap
|
|
258
|
+
" Step 1: Declare with correct interface type, then assign
|
|
259
|
+
DATA lo_mock TYPE REF TO zif_my_interface.
|
|
260
|
+
lo_mock ?= cl_abap_testdouble=>create( 'ZIF_MY_INTERFACE' ).
|
|
261
|
+
|
|
262
|
+
" Step 2: Configure return value - use returning() not IMPORTING
|
|
263
|
+
cl_abap_testdouble=>configure_call( lo_mock )->returning( lo_mock_result ).
|
|
264
|
+
|
|
265
|
+
" Step 3: Call method to register configuration (MUST use same params in test)
|
|
266
|
+
lo_mock->my_method(
|
|
267
|
+
EXPORTING
|
|
268
|
+
iv_param1 = 'value1'
|
|
269
|
+
iv_param2 = 'value2' ).
|
|
270
|
+
|
|
271
|
+
" Step 4: In test, call with SAME parameters as registered above
|
|
272
|
+
DATA(ls_result) = lo_mock->my_method(
|
|
273
|
+
EXPORTING
|
|
274
|
+
iv_param1 = 'value1'
|
|
275
|
+
iv_param2 = 'value2' ).
|
|
276
|
+
|
|
277
|
+
" To raise exception:
|
|
278
|
+
DATA(lx_error) = NEW zcx_my_exception( ).
|
|
279
|
+
cl_abap_testdouble=>configure_call( lo_mock )->raise_exception( lx_error ).
|
|
280
|
+
lo_mock->my_method( ... ).
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
**Important Notes:**
|
|
284
|
+
- Parameters in configure_call registration MUST match parameters in test execution
|
|
285
|
+
- Always declare variable with interface type first: `DATA lo_mock TYPE REF TO zif_xxx`
|
|
286
|
+
- Use `returning(value = ...)` not `IMPORTING`
|
|
287
|
+
- Call method after configure_call to register the configuration
|
|
288
|
+
|
|
289
|
+
### Mocking EXPORT Parameters
|
|
290
|
+
|
|
291
|
+
Some methods use EXPORT parameters instead of returning values. Use `set_parameter`:
|
|
292
|
+
|
|
293
|
+
```abap
|
|
294
|
+
" Mock EXPORT parameter EI_REPO
|
|
295
|
+
cl_abap_testdouble=>configure_call( lo_repo_srv )->set_parameter(
|
|
296
|
+
EXPORTING
|
|
297
|
+
name = 'EI_REPO'
|
|
298
|
+
value = lo_repo_double ).
|
|
299
|
+
|
|
300
|
+
" Register the method call
|
|
301
|
+
lo_repo_srv->get_repo_from_url(
|
|
302
|
+
EXPORTING iv_url = 'https://github.com/test/repo.git' ).
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Mocking Inherited Methods
|
|
306
|
+
|
|
307
|
+
When an interface extends another interface, use the parent interface prefix:
|
|
308
|
+
|
|
309
|
+
```abap
|
|
310
|
+
" zif_abapgit_repo_online extends zif_abapgit_repo
|
|
311
|
+
" Call inherited method with prefix
|
|
312
|
+
lo_repo->zif_abapgit_repo~get_package( ).
|
|
313
|
+
lo_repo->zif_abapgit_repo~refresh( ).
|
|
314
|
+
lo_repo->zif_abapgit_repo~get_files_local( ).
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### Mocking Methods with No Parameters
|
|
318
|
+
|
|
319
|
+
When source code calls a method with no parameters:
|
|
320
|
+
|
|
321
|
+
```abap
|
|
322
|
+
" Configure returning (no method name)
|
|
323
|
+
cl_abap_testdouble=>configure_call( lo_mock )->returning( lt_data ).
|
|
324
|
+
|
|
325
|
+
" Register with no parameters (matches source code)
|
|
326
|
+
lo_mock->get_files_local( ).
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### Common Mistakes
|
|
330
|
+
|
|
331
|
+
| Mistake | Correction |
|
|
332
|
+
|---------|------------|
|
|
333
|
+
| Using `IMPORTING` in configure_call | Use `returning()` or `set_parameter()` |
|
|
334
|
+
| Calling method inside configure_call | Call method separately after configure_call |
|
|
335
|
+
| Wrong parameter count | Match exactly what source code calls |
|
|
336
|
+
| Forgot to mock a method | Mock ALL methods the code under test calls |
|
|
337
|
+
| Interface prefix not used | Use `zif_parent~method()` for inherited methods |
|
|
338
|
+
| Didn't check source code first | ALWAYS read source code to see how method is called |
|
|
339
|
+
| Cannot add RAISING to interface method | Use TRY..CATCH to handle exceptions in implementation |
|
|
340
|
+
|
|
341
|
+
### Handling Exceptions in Interface Implementation
|
|
342
|
+
|
|
343
|
+
When implementing an interface method that calls other methods raising exceptions:
|
|
344
|
+
|
|
345
|
+
- **DO NOT** add RAISING to the interface method - you cannot change the interface
|
|
346
|
+
- **USE** TRY..CATCH to catch and handle exceptions within the implementation
|
|
347
|
+
|
|
348
|
+
```abap
|
|
349
|
+
" Interface method does NOT declare RAISING
|
|
350
|
+
METHOD zif_abgagt_command~execute.
|
|
351
|
+
|
|
352
|
+
" Method being called can raise exception
|
|
353
|
+
TRY.
|
|
354
|
+
get_user( )->set_repo_git_user_name( ... ).
|
|
355
|
+
CATCH zcx_abapgit_exception INTO DATA(lx_error).
|
|
356
|
+
rv_result = '{"error":"' && lx_error->get_text( ) && '"}'.
|
|
357
|
+
RETURN.
|
|
358
|
+
ENDTRY.
|
|
359
|
+
|
|
360
|
+
ENDMETHOD.
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### Important: Read Source Code First
|
|
364
|
+
|
|
365
|
+
**ALWAYS check the source code to see HOW a method is called before writing tests:**
|
|
366
|
+
|
|
367
|
+
1. Check what parameters are passed (none, some, or all)
|
|
368
|
+
2. Check if optional parameters are used
|
|
369
|
+
3. Check if parameters have default values
|
|
370
|
+
4. Check for type casts (e.g., `li_repo_online ?= li_repo`)
|
|
371
|
+
|
|
372
|
+
```abap
|
|
373
|
+
" Source code line 122:
|
|
374
|
+
lt_files = li_repo->get_files_local( ).
|
|
375
|
+
|
|
376
|
+
" Test MUST match - no parameters!
|
|
377
|
+
cl_abap_testdouble=>configure_call( lo_repo )->returning( lt_empty_files ).
|
|
378
|
+
lo_repo->get_files_local( ). " No parameters!
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
If the source code calls `get_files_local( )` with no parameters, your test registration must also have no parameters. Even if the method signature has optional parameters, if the source doesn't pass them, your mock registration must not pass them either.
|
|
382
|
+
|
|
383
|
+
---
|
|
384
|
+
|
|
385
|
+
## Guidelines for AI Coding Tools
|
|
386
|
+
|
|
387
|
+
When creating new ABAP classes, ALWAYS follow these rules:
|
|
388
|
+
|
|
389
|
+
### DO:
|
|
390
|
+
|
|
391
|
+
1. **Use interfaces for ALL external dependencies**
|
|
392
|
+
- Database access → interface
|
|
393
|
+
- External API calls → interface
|
|
394
|
+
- File I/O → interface
|
|
395
|
+
- Other services → interface
|
|
396
|
+
|
|
397
|
+
2. **Pass dependencies via constructor**
|
|
398
|
+
```abap
|
|
399
|
+
METHODS constructor
|
|
400
|
+
IMPORTING
|
|
401
|
+
io_agent TYPE REF TO zif_abgagt_agent
|
|
402
|
+
io_logger TYPE REF TO zif_logger.
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
3. **Define interfaces for all service classes**
|
|
406
|
+
```abap
|
|
407
|
+
" Instead of using class directly
|
|
408
|
+
DATA mo_agent TYPE REF TO zcl_abgagt_agent. " BAD
|
|
409
|
+
|
|
410
|
+
" Use interface
|
|
411
|
+
DATA mo_agent TYPE REF TO zif_abgagt_agent. " GOOD
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
4. **Keep classes FINAL if they don't need mocking**
|
|
415
|
+
- If a class has no dependencies and doesn't need test doubles, make it FINAL
|
|
416
|
+
- If a class needs to be mocked in tests, don't make it FINAL
|
|
417
|
+
|
|
418
|
+
5. **Use dependency injection in command classes**
|
|
419
|
+
```abap
|
|
420
|
+
" Good pattern for command classes
|
|
421
|
+
CLASS zcl_abgagt_command_pull DEFINITION PUBLIC.
|
|
422
|
+
PUBLIC SECTION.
|
|
423
|
+
INTERFACES zif_abgagt_command.
|
|
424
|
+
METHODS constructor
|
|
425
|
+
IMPORTING io_agent TYPE REF TO zif_abgagt_agent.
|
|
426
|
+
ENDCLASS.
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### DON'T:
|
|
430
|
+
|
|
431
|
+
1. **Never create dependencies inside methods**
|
|
432
|
+
```abap
|
|
433
|
+
" BAD
|
|
434
|
+
METHOD execute.
|
|
435
|
+
lo_agent = NEW zcl_abgagt_agent( ). " Hardcoded!
|
|
436
|
+
ENDMETHOD.
|
|
437
|
+
|
|
438
|
+
" GOOD
|
|
439
|
+
METHOD execute.
|
|
440
|
+
ls_result = mo_agent->pull( ... ). " Use injected
|
|
441
|
+
ENDMETHOD.
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
2. **Don't use static method calls for testable code**
|
|
445
|
+
```abap
|
|
446
|
+
" BAD
|
|
447
|
+
DATA(lo_srv) = zcl_some_srv=>get_instance( ).
|
|
448
|
+
|
|
449
|
+
" GOOD - inject the service
|
|
450
|
+
DATA(lo_srv) = mo_service_provider.
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
3. **Don't make classes FINAL if they need test doubles**
|
|
454
|
+
- If you need to mock a class in tests, don't declare it FINAL
|
|
455
|
+
|
|
456
|
+
---
|
|
457
|
+
|
|
458
|
+
## Example: Refactoring for Testability
|
|
459
|
+
|
|
460
|
+
### Before (Not Testable)
|
|
461
|
+
|
|
462
|
+
```abap
|
|
463
|
+
CLASS zcl_abgagt_command_pull DEFINITION PUBLIC.
|
|
464
|
+
METHOD execute.
|
|
465
|
+
DATA lo_agent TYPE REF TO zcl_abgagt_agent.
|
|
466
|
+
lo_agent = NEW zcl_abgagt_agent( ). " Hardcoded!
|
|
467
|
+
|
|
468
|
+
ls_result = lo_agent->pull(
|
|
469
|
+
iv_url = ls_params-url
|
|
470
|
+
iv_branch = ls_params-branch ).
|
|
471
|
+
ENDMETHOD.
|
|
472
|
+
ENDCLASS.
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
### After (Testable)
|
|
476
|
+
|
|
477
|
+
```abap
|
|
478
|
+
" Interface for agent
|
|
479
|
+
INTERFACE zif_abgagt_agent PUBLIC.
|
|
480
|
+
METHODS pull ... RAISING zcx_abapgit_exception.
|
|
481
|
+
ENDINTERFACE.
|
|
482
|
+
|
|
483
|
+
" Command class with constructor injection
|
|
484
|
+
CLASS zcl_abgagt_command_pull DEFINITION PUBLIC.
|
|
485
|
+
PUBLIC SECTION.
|
|
486
|
+
INTERFACES zif_abgagt_command.
|
|
487
|
+
|
|
488
|
+
METHODS constructor
|
|
489
|
+
IMPORTING
|
|
490
|
+
io_agent TYPE REF TO zif_abgagt_agent OPTIONAL. " Optional for backward compat
|
|
491
|
+
|
|
492
|
+
PRIVATE SECTION.
|
|
493
|
+
DATA mo_agent TYPE REF TO zif_abgagt_agent.
|
|
494
|
+
|
|
495
|
+
METHODS get_agent
|
|
496
|
+
RETURNING VALUE(ro_agent) TYPE REF TO zif_abgagt_agent.
|
|
497
|
+
ENDCLASS.
|
|
498
|
+
|
|
499
|
+
CLASS zcl_abgagt_command_pull IMPLEMENTATION.
|
|
500
|
+
|
|
501
|
+
METHOD constructor.
|
|
502
|
+
mo_agent = io_agent.
|
|
503
|
+
ENDMETHOD.
|
|
504
|
+
|
|
505
|
+
METHOD get_agent.
|
|
506
|
+
" Lazy creation if not injected (for production)
|
|
507
|
+
IF mo_agent IS NOT BOUND.
|
|
508
|
+
mo_agent = NEW zcl_abgagt_agent( ).
|
|
509
|
+
ENDIF.
|
|
510
|
+
ro_agent = mo_agent.
|
|
511
|
+
ENDMETHOD.
|
|
512
|
+
|
|
513
|
+
METHOD execute.
|
|
514
|
+
DATA(lo_agent) = get_agent( ).
|
|
515
|
+
ls_result = lo_agent->pull( ... ).
|
|
516
|
+
ENDMETHOD.
|
|
517
|
+
|
|
518
|
+
ENDCLASS.
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
**Production usage:**
|
|
522
|
+
```abap
|
|
523
|
+
DATA(lo_command) = NEW zcl_abgagt_command_pull(
|
|
524
|
+
io_agent = NEW zcl_abgagt_agent( ) ).
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
**Test usage:**
|
|
528
|
+
```abap
|
|
529
|
+
CLASS ltd_mock_agent DEFINITION FOR TESTING.
|
|
530
|
+
PUBLIC SECTION.
|
|
531
|
+
INTERFACES zif_abgagt_agent PARTIALLY IMPLEMENTED.
|
|
532
|
+
ENDCLASS.
|
|
533
|
+
|
|
534
|
+
CLASS ltd_mock_agent IMPLEMENTATION.
|
|
535
|
+
METHOD zif_abgagt_agent~pull.
|
|
536
|
+
rs_result-success = abap_true.
|
|
537
|
+
rs_result-message = 'Mocked success'.
|
|
538
|
+
ENDMETHOD.
|
|
539
|
+
ENDCLASS.
|
|
540
|
+
|
|
541
|
+
CLASS ltcl_test DEFINITION FOR TESTING.
|
|
542
|
+
METHOD test_pull_success.
|
|
543
|
+
DATA(lo_mock) = NEW ltd_mock_agent( ).
|
|
544
|
+
DATA(lo_cut) = NEW zcl_abgagt_command_pull( io_agent = lo_mock ).
|
|
545
|
+
|
|
546
|
+
DATA(lv_result) = lo_cut->execute( ... ).
|
|
547
|
+
|
|
548
|
+
" Assert mocked behavior
|
|
549
|
+
ENDMETHOD.
|
|
550
|
+
ENDCLASS.
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
---
|
|
554
|
+
|
|
555
|
+
## Key Takeaways
|
|
556
|
+
|
|
557
|
+
1. **Always use interfaces** for dependencies
|
|
558
|
+
2. **Use constructor injection** to pass dependencies
|
|
559
|
+
3. **Never hardcode `NEW` for dependencies** - pass them in
|
|
560
|
+
4. **Avoid static calls** - use instance methods with injected dependencies
|
|
561
|
+
5. **Keep constructors simple** - only assign dependencies
|
|
562
|
+
|
|
563
|
+
Following these guidelines ensures that:
|
|
564
|
+
- Unit tests can mock all dependencies
|
|
565
|
+
- Tests run fast without external systems
|
|
566
|
+
- Tests are reliable and repeatable
|
|
567
|
+
- Error conditions can be tested easily
|
|
568
|
+
- Code is modular and loosely coupled
|
package/bin/abapgit-agent
CHANGED
|
@@ -538,12 +538,14 @@ async function processInspectResult(res) {
|
|
|
538
538
|
const errorCount = res.ERROR_COUNT !== undefined ? res.ERROR_COUNT : (res.error_count || 0);
|
|
539
539
|
const errors = res.ERRORS !== undefined ? res.ERRORS : (res.errors || []);
|
|
540
540
|
const warnings = res.WARNINGS !== undefined ? res.WARNINGS : (res.warnings || []);
|
|
541
|
+
const infos = res.INFOS !== undefined ? res.INFOS : (res.infos || []);
|
|
541
542
|
|
|
542
|
-
if (errorCount > 0 || warnings.length > 0) {
|
|
543
|
+
if (errorCount > 0 || warnings.length > 0 || infos.length > 0) {
|
|
543
544
|
if (errorCount > 0) {
|
|
544
545
|
console.log(`❌ ${objectType} ${objectName} - Syntax check failed (${errorCount} error(s)):`);
|
|
545
546
|
} else {
|
|
546
|
-
|
|
547
|
+
const total = warnings.length + infos.length;
|
|
548
|
+
console.log(`⚠️ ${objectType} ${objectName} - Syntax check passed with warnings (${total}):`);
|
|
547
549
|
}
|
|
548
550
|
console.log('\nErrors:');
|
|
549
551
|
console.log('─'.repeat(60));
|
|
@@ -552,8 +554,16 @@ async function processInspectResult(res) {
|
|
|
552
554
|
const line = err.LINE || err.line || '?';
|
|
553
555
|
const column = err.COLUMN || err.column || '?';
|
|
554
556
|
const text = err.TEXT || err.text || 'Unknown error';
|
|
557
|
+
const methodName = err.METHOD_NAME || err.method_name;
|
|
558
|
+
const sobjname = err.SOBJNAME || err.sobjname;
|
|
555
559
|
|
|
560
|
+
if (methodName) {
|
|
561
|
+
console.log(` Method: ${methodName}`);
|
|
562
|
+
}
|
|
556
563
|
console.log(` Line ${line}, Column ${column}:`);
|
|
564
|
+
if (sobjname && sobjname.includes('====')) {
|
|
565
|
+
console.log(` Include: ${sobjname}`);
|
|
566
|
+
}
|
|
557
567
|
console.log(` ${text}`);
|
|
558
568
|
console.log('');
|
|
559
569
|
}
|
|
@@ -565,7 +575,38 @@ async function processInspectResult(res) {
|
|
|
565
575
|
for (const warn of warnings) {
|
|
566
576
|
const line = warn.LINE || warn.line || '?';
|
|
567
577
|
const text = warn.MESSAGE || warn.message || 'Unknown warning';
|
|
568
|
-
|
|
578
|
+
const methodName = warn.METHOD_NAME || warn.method_name;
|
|
579
|
+
const sobjname = warn.SOBJNAME || warn.sobjname;
|
|
580
|
+
|
|
581
|
+
if (methodName) {
|
|
582
|
+
console.log(` Method: ${methodName}`);
|
|
583
|
+
}
|
|
584
|
+
console.log(` Line ${line}:`);
|
|
585
|
+
if (sobjname && sobjname.includes('====')) {
|
|
586
|
+
console.log(` Include: ${sobjname}`);
|
|
587
|
+
}
|
|
588
|
+
console.log(` ${text}`);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Show infos if any
|
|
593
|
+
if (infos.length > 0) {
|
|
594
|
+
console.log('Info:');
|
|
595
|
+
console.log('─'.repeat(60));
|
|
596
|
+
for (const info of infos) {
|
|
597
|
+
const line = info.LINE || info.line || '?';
|
|
598
|
+
const text = info.MESSAGE || info.message || 'Unknown info';
|
|
599
|
+
const methodName = info.METHOD_NAME || info.method_name;
|
|
600
|
+
const sobjname = info.SOBJNAME || info.sobjname;
|
|
601
|
+
|
|
602
|
+
if (methodName) {
|
|
603
|
+
console.log(` Method: ${methodName}`);
|
|
604
|
+
}
|
|
605
|
+
console.log(` Line ${line}:`);
|
|
606
|
+
if (sobjname && sobjname.includes('====')) {
|
|
607
|
+
console.log(` Include: ${sobjname}`);
|
|
608
|
+
}
|
|
609
|
+
console.log(` ${text}`);
|
|
569
610
|
}
|
|
570
611
|
}
|
|
571
612
|
} else if (success === true || success === 'X') {
|
|
@@ -652,7 +693,7 @@ async function runUnitTests(options) {
|
|
|
652
693
|
/**
|
|
653
694
|
* Run unit test for a single file
|
|
654
695
|
*/
|
|
655
|
-
async function runUnitTestForFile(sourceFile, csrfToken, config) {
|
|
696
|
+
async function runUnitTestForFile(sourceFile, csrfToken, config, coverage = false) {
|
|
656
697
|
console.log(` Running unit test for: ${sourceFile}`);
|
|
657
698
|
|
|
658
699
|
try {
|
|
@@ -687,7 +728,8 @@ async function runUnitTestForFile(sourceFile, csrfToken, config) {
|
|
|
687
728
|
|
|
688
729
|
// Send files array to unit endpoint (ABAP expects string_table of file names)
|
|
689
730
|
const data = {
|
|
690
|
-
files: [sourceFile]
|
|
731
|
+
files: [sourceFile],
|
|
732
|
+
coverage: coverage
|
|
691
733
|
};
|
|
692
734
|
|
|
693
735
|
const result = await request('POST', '/sap/bc/z_abapgit_agent/unit', data, { csrfToken });
|
|
@@ -700,6 +742,9 @@ async function runUnitTestForFile(sourceFile, csrfToken, config) {
|
|
|
700
742
|
const message = result.MESSAGE || result.message || '';
|
|
701
743
|
const errors = result.ERRORS || result.errors || [];
|
|
702
744
|
|
|
745
|
+
// Handle coverage data
|
|
746
|
+
const coverageStats = result.COVERAGE_STATS || result.coverage_stats;
|
|
747
|
+
|
|
703
748
|
if (testCount === 0) {
|
|
704
749
|
console.log(` ➖ ${objName} - No unit tests`);
|
|
705
750
|
} else if (success === 'X' || success === true) {
|
|
@@ -710,6 +755,17 @@ async function runUnitTestForFile(sourceFile, csrfToken, config) {
|
|
|
710
755
|
|
|
711
756
|
console.log(` Tests: ${testCount} | Passed: ${passedCount} | Failed: ${failedCount}`);
|
|
712
757
|
|
|
758
|
+
// Display coverage if available
|
|
759
|
+
if (coverage && coverageStats) {
|
|
760
|
+
const totalLines = coverageStats.TOTAL_LINES || coverageStats.total_lines || 0;
|
|
761
|
+
const coveredLines = coverageStats.COVERED_LINES || coverageStats.covered_lines || 0;
|
|
762
|
+
const coverageRate = coverageStats.COVERAGE_RATE || coverageStats.coverage_rate || 0;
|
|
763
|
+
|
|
764
|
+
console.log(` 📊 Coverage: ${coverageRate}%`);
|
|
765
|
+
console.log(` Total Lines: ${totalLines}`);
|
|
766
|
+
console.log(` Covered Lines: ${coveredLines}`);
|
|
767
|
+
}
|
|
768
|
+
|
|
713
769
|
if (failedCount > 0 && errors.length > 0) {
|
|
714
770
|
for (const err of errors) {
|
|
715
771
|
const className = err.CLASS_NAME || err.class_name || '?';
|
|
@@ -1727,21 +1783,25 @@ Examples:
|
|
|
1727
1783
|
const filesArgIndex = args.indexOf('--files');
|
|
1728
1784
|
if (filesArgIndex === -1 || filesArgIndex + 1 >= args.length) {
|
|
1729
1785
|
console.error('Error: --files parameter required');
|
|
1730
|
-
console.error('Usage: abapgit-agent unit --files <file1>,<file2>,...');
|
|
1786
|
+
console.error('Usage: abapgit-agent unit --files <file1>,<file2>,... [--coverage]');
|
|
1731
1787
|
console.error('Example: abapgit-agent unit --files zcl_my_test.clas.abap');
|
|
1788
|
+
console.error('Example: abapgit-agent unit --files zcl_my_test.clas.abap --coverage');
|
|
1732
1789
|
process.exit(1);
|
|
1733
1790
|
}
|
|
1734
1791
|
|
|
1735
1792
|
const files = args[filesArgIndex + 1].split(',').map(f => f.trim());
|
|
1736
1793
|
|
|
1737
|
-
|
|
1794
|
+
// Check for coverage option
|
|
1795
|
+
const coverage = args.includes('--coverage');
|
|
1796
|
+
|
|
1797
|
+
console.log(`\n Running unit tests for ${files.length} file(s)${coverage ? ' (with coverage)' : ''}`);
|
|
1738
1798
|
console.log('');
|
|
1739
1799
|
|
|
1740
1800
|
const config = loadConfig();
|
|
1741
1801
|
const csrfToken = await fetchCsrfToken(config);
|
|
1742
1802
|
|
|
1743
1803
|
for (const sourceFile of files) {
|
|
1744
|
-
await runUnitTestForFile(sourceFile, csrfToken, config);
|
|
1804
|
+
await runUnitTestForFile(sourceFile, csrfToken, config, coverage);
|
|
1745
1805
|
}
|
|
1746
1806
|
break;
|
|
1747
1807
|
}
|
|
@@ -2452,32 +2512,10 @@ Examples:
|
|
|
2452
2512
|
const pattern = args[patternIndex];
|
|
2453
2513
|
const result = await refSearch.searchPattern(pattern);
|
|
2454
2514
|
|
|
2455
|
-
// Also search guidelines if available
|
|
2456
|
-
const guidelinesResult = await refSearch.searchGuidelines(pattern);
|
|
2457
|
-
if (guidelinesResult && guidelinesResult.guidelinesFound && guidelinesResult.matches.length > 0) {
|
|
2458
|
-
result.guidelines = guidelinesResult;
|
|
2459
|
-
}
|
|
2460
|
-
|
|
2461
2515
|
if (jsonOutput) {
|
|
2462
2516
|
console.log(JSON.stringify(result, null, 2));
|
|
2463
2517
|
} else {
|
|
2464
2518
|
refSearch.displaySearchResults(result);
|
|
2465
|
-
// Display guidelines results if found
|
|
2466
|
-
if (guidelinesResult && guidelinesResult.guidelinesFound && guidelinesResult.matches.length > 0) {
|
|
2467
|
-
console.log('\n 📋 Custom Guidelines:');
|
|
2468
|
-
for (const match of guidelinesResult.matches.slice(0, 5)) {
|
|
2469
|
-
console.log(` 📄 ${match.file} (line ${match.line}):`);
|
|
2470
|
-
const lines = match.context.split('\n');
|
|
2471
|
-
lines.forEach((line, idx) => {
|
|
2472
|
-
const prefix = idx === 1 ? ' → ' : ' ';
|
|
2473
|
-
const trimmed = line.slice(0, 80);
|
|
2474
|
-
console.log(`${prefix}${trimmed}`);
|
|
2475
|
-
});
|
|
2476
|
-
}
|
|
2477
|
-
if (guidelinesResult.matches.length > 5) {
|
|
2478
|
-
console.log(` ... and ${guidelinesResult.matches.length - 5} more matches`);
|
|
2479
|
-
}
|
|
2480
|
-
}
|
|
2481
2519
|
}
|
|
2482
2520
|
break;
|
|
2483
2521
|
}
|
package/package.json
CHANGED
package/src/config.js
CHANGED
|
@@ -24,7 +24,7 @@ function loadConfig() {
|
|
|
24
24
|
if (fs.existsSync(configPath)) {
|
|
25
25
|
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
26
26
|
} else {
|
|
27
|
-
// Load from environment variables
|
|
27
|
+
// Load from environment variables only when no config file exists
|
|
28
28
|
config = {
|
|
29
29
|
host: process.env.ABAP_HOST,
|
|
30
30
|
sapport: parseInt(process.env.ABAP_PORT, 10) || 443,
|
package/src/ref-search.js
CHANGED
|
@@ -257,79 +257,117 @@ async function getSearchableFiles(repoPath, repoName, extensions = ['.md', '.aba
|
|
|
257
257
|
}
|
|
258
258
|
|
|
259
259
|
/**
|
|
260
|
-
* Search for a pattern across all reference repositories
|
|
260
|
+
* Search for a pattern across all reference repositories and local guidelines
|
|
261
261
|
* @param {string} pattern - Pattern to search for
|
|
262
262
|
* @returns {Promise<Object>} Search results
|
|
263
263
|
*/
|
|
264
264
|
async function searchPattern(pattern) {
|
|
265
265
|
const refFolder = detectReferenceFolder();
|
|
266
266
|
const repos = await getReferenceRepositories();
|
|
267
|
+
const guidelinesFolder = detectGuidelinesFolder();
|
|
267
268
|
|
|
268
|
-
|
|
269
|
+
// If neither reference folder nor guidelines exist, return error
|
|
270
|
+
if (!refFolder && !guidelinesFolder) {
|
|
269
271
|
return {
|
|
270
272
|
error: 'Reference folder not found',
|
|
271
|
-
hint: 'Configure referenceFolder in .abapGitAgent
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
if (repos.length === 0) {
|
|
276
|
-
return {
|
|
277
|
-
error: 'No ABAP repositories found in reference folder',
|
|
278
|
-
hint: 'Clone ABAP repositories to the reference folder to enable searching'
|
|
273
|
+
hint: 'Configure referenceFolder in .abapGitAgent, clone to ~/abap-reference, or create abap/guidelines/ folder'
|
|
279
274
|
};
|
|
280
275
|
}
|
|
281
276
|
|
|
282
277
|
const results = {
|
|
283
278
|
pattern,
|
|
284
279
|
referenceFolder: refFolder,
|
|
280
|
+
guidelinesFolder: guidelinesFolder,
|
|
285
281
|
repositories: repos.map(r => r.name),
|
|
286
282
|
files: [],
|
|
287
283
|
matches: []
|
|
288
284
|
};
|
|
289
285
|
|
|
290
286
|
try {
|
|
291
|
-
// Search
|
|
292
|
-
|
|
293
|
-
const
|
|
287
|
+
// Search reference repositories if available
|
|
288
|
+
if (repos.length > 0) {
|
|
289
|
+
for (const repo of repos) {
|
|
290
|
+
const searchableFiles = await getSearchableFiles(repo.path, repo.name);
|
|
291
|
+
|
|
292
|
+
for (const fileInfo of searchableFiles) {
|
|
293
|
+
try {
|
|
294
|
+
const content = await readFile(fileInfo.path, 'utf8');
|
|
295
|
+
|
|
296
|
+
if (content.toLowerCase().includes(pattern.toLowerCase())) {
|
|
297
|
+
results.files.push({
|
|
298
|
+
repo: repo.name,
|
|
299
|
+
file: fileInfo.relativePath
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// Find matching lines with context
|
|
303
|
+
const lines = content.split('\n');
|
|
304
|
+
let matchCount = 0;
|
|
305
|
+
|
|
306
|
+
for (let i = 0; i < lines.length; i++) {
|
|
307
|
+
if (lines[i].toLowerCase().includes(pattern.toLowerCase())) {
|
|
308
|
+
const start = Math.max(0, i - 1);
|
|
309
|
+
const end = Math.min(lines.length, i + 2);
|
|
310
|
+
const context = lines.slice(start, end).join('\n');
|
|
311
|
+
|
|
312
|
+
results.matches.push({
|
|
313
|
+
repo: repo.name,
|
|
314
|
+
file: fileInfo.relativePath,
|
|
315
|
+
line: i + 1,
|
|
316
|
+
context
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
matchCount++;
|
|
320
|
+
|
|
321
|
+
// Limit matches per file to avoid overwhelming output
|
|
322
|
+
if (matchCount >= 3) {
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
} catch (error) {
|
|
329
|
+
// Skip files we can't read
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
294
334
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
335
|
+
// Search local guidelines folder if available
|
|
336
|
+
if (guidelinesFolder) {
|
|
337
|
+
const guidelineFiles = await getGuidelineFiles();
|
|
298
338
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
339
|
+
for (const file of guidelineFiles) {
|
|
340
|
+
if (file.content.toLowerCase().includes(pattern.toLowerCase())) {
|
|
341
|
+
results.files.push({
|
|
342
|
+
repo: 'guidelines',
|
|
343
|
+
file: file.relativePath
|
|
344
|
+
});
|
|
304
345
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
346
|
+
// Find matching lines with context
|
|
347
|
+
const lines = file.content.split('\n');
|
|
348
|
+
let matchCount = 0;
|
|
308
349
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
350
|
+
for (let i = 0; i < lines.length; i++) {
|
|
351
|
+
if (lines[i].toLowerCase().includes(pattern.toLowerCase())) {
|
|
352
|
+
const start = Math.max(0, i - 1);
|
|
353
|
+
const end = Math.min(lines.length, i + 2);
|
|
354
|
+
const context = lines.slice(start, end).join('\n');
|
|
314
355
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
356
|
+
results.matches.push({
|
|
357
|
+
repo: 'guidelines',
|
|
358
|
+
file: file.relativePath,
|
|
359
|
+
line: i + 1,
|
|
360
|
+
context
|
|
361
|
+
});
|
|
321
362
|
|
|
322
|
-
|
|
363
|
+
matchCount++;
|
|
323
364
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
}
|
|
365
|
+
// Limit matches per file to avoid overwhelming output
|
|
366
|
+
if (matchCount >= 3) {
|
|
367
|
+
break;
|
|
328
368
|
}
|
|
329
369
|
}
|
|
330
370
|
}
|
|
331
|
-
} catch (error) {
|
|
332
|
-
// Skip files we can't read
|
|
333
371
|
}
|
|
334
372
|
}
|
|
335
373
|
}
|
|
@@ -469,7 +507,16 @@ function displaySearchResults(results) {
|
|
|
469
507
|
}
|
|
470
508
|
|
|
471
509
|
console.log(`\n 🔍 Searching for: '${results.pattern}'`);
|
|
472
|
-
|
|
510
|
+
|
|
511
|
+
// Show which sources were searched
|
|
512
|
+
const sources = [];
|
|
513
|
+
if (results.referenceFolder) {
|
|
514
|
+
sources.push('reference repositories');
|
|
515
|
+
}
|
|
516
|
+
if (results.guidelinesFolder) {
|
|
517
|
+
sources.push('local guidelines');
|
|
518
|
+
}
|
|
519
|
+
console.log(` 📁 Sources searched: ${sources.join(', ') || 'none'}`);
|
|
473
520
|
|
|
474
521
|
if (results.repositories && results.repositories.length > 0) {
|
|
475
522
|
console.log(` 📚 Repositories (${results.repositories.length}): ${results.repositories.join(', ')}`);
|
|
@@ -491,7 +538,8 @@ function displaySearchResults(results) {
|
|
|
491
538
|
|
|
492
539
|
console.log(` ✅ Found in ${results.files.length} file(s):`);
|
|
493
540
|
for (const [repo, files] of Object.entries(filesByRepo)) {
|
|
494
|
-
|
|
541
|
+
const icon = repo === 'guidelines' ? '📋' : '📦';
|
|
542
|
+
console.log(`\n ${icon} ${repo}/`);
|
|
495
543
|
files.forEach(file => {
|
|
496
544
|
console.log(` • ${file}`);
|
|
497
545
|
});
|