1
- import { CodeAction , CodeActionKind , CodeActionParams , Position , Range , TextEdit } from 'vscode-languageserver' ;
1
+ import { CodeAction , CodeActionKind , CodeActionParams , CreateFile , Position , Range , TextDocumentEdit , TextEdit , WorkspaceFolder } from 'vscode-languageserver' ;
2
2
import { TextDocument } from 'vscode-languageserver-textdocument' ;
3
3
import { documents , parser , prettyKeywords } from '.' ;
4
- import Cache from '../../../../language/models/cache' ;
4
+ import Cache , { RpgleTypeDetail , RpgleVariableType } from '../../../../language/models/cache' ;
5
5
import { getLinterCodeActions } from './linter/codeActions' ;
6
6
import { createExtract , caseInsensitiveReplaceAll } from './language' ;
7
+ import { Keywords } from '../../../../language/parserTypes' ;
8
+ import path = require( 'path' ) ;
9
+ import { getWorkspaceFolder } from '../connection' ;
10
+ import Declaration from '../../../../language/models/declaration' ;
11
+
12
+ interface TestCaseSpec {
13
+ prototype : string [ ] ;
14
+ testCase : string [ ] ;
15
+ includes : string [ ] ;
16
+ }
7
17
8
18
export default async function genericCodeActionsProvider ( params : CodeActionParams ) : Promise < CodeAction [ ] | undefined > {
9
19
const uri = params . textDocument . uri ;
@@ -33,10 +43,10 @@ export default async function genericCodeActionsProvider(params: CodeActionParam
33
43
}
34
44
}
35
45
36
- // const testCaseOption = getTestCaseAction (document, docs, range);
37
- // if (testCaseOption ) {
38
- // actions.push(testCaseOption );
39
- // }
46
+ const testActions = await getTestActions ( document , docs , range ) ;
47
+ if ( testActions ) {
48
+ actions . push ( ... testActions ) ;
49
+ }
40
50
41
51
const monitorAction = surroundWithMonitorAction ( isFree , document , docs , range ) ;
42
52
if ( monitorAction ) {
@@ -48,32 +58,282 @@ export default async function genericCodeActionsProvider(params: CodeActionParam
48
58
return actions ;
49
59
}
50
60
51
- export function getTestCaseAction ( document : TextDocument , docs : Cache , range : Range ) : CodeAction | undefined {
52
- const currentProcedure = docs . procedures . find ( sub => range . start . line >= sub . position . range . line && sub . range . start && sub . range . end ) ;
53
- if ( currentProcedure ) {
61
+ export async function getTestActions ( document : TextDocument , docs : Cache , range : Range ) : Promise < CodeAction [ ] | undefined > {
62
+ const codeActions : CodeAction [ ] = [ ] ;
63
+
64
+ const exportProcedures = docs . procedures . filter ( proc => proc . keyword [ `EXPORT` ] ) ;
65
+ if ( exportProcedures . length > 0 ) {
66
+ const workspaceFolder = await getWorkspaceFolder ( document . uri ) ; // TODO: Can workspace folder not be a requirement?
67
+ if ( workspaceFolder ) {
68
+ // Build new test file uri
69
+ const parsedPath = path . parse ( document . uri ) ;
70
+ const fileName = parsedPath . base ;
71
+ const testFileName = `${ parsedPath . name } .test${ parsedPath . ext } ` ;
72
+ const testFileUri = workspaceFolder ?
73
+ `${ workspaceFolder . uri } /qtestsrc/${ testFileName } ` :
74
+ `${ parsedPath . dir } /${ testFileName } ` ;
75
+
76
+ // Test case generation
77
+ const currentProcedure = exportProcedures . find ( sub => sub . range . start && sub . range . end && range . start . line >= sub . range . start && range . end . line <= sub . range . end ) ;
78
+ if ( currentProcedure ) {
79
+ const testCaseSpec = await getTestCaseSpec ( docs , currentProcedure , workspaceFolder ) ;
80
+ const newTestSuite = generateTestSuite ( [ testCaseSpec ] ) ;
81
+ const testCaseAction = CodeAction . create ( `Generate test case for '${ currentProcedure . name } '` , CodeActionKind . RefactorExtract ) ;
82
+ testCaseAction . edit = {
83
+ documentChanges : [
84
+ CreateFile . create ( testFileUri , { ignoreIfExists : true } ) ,
85
+ TextDocumentEdit . create ( { uri : testFileUri , version : null } , [ TextEdit . insert ( Position . create ( 0 , 0 ) , newTestSuite . join ( `\n` ) ) ] )
86
+ ]
87
+ } ;
88
+ codeActions . push ( testCaseAction ) ;
89
+ }
54
90
55
- const refactorAction = CodeAction . create ( `Create IBM i test case` , CodeActionKind . RefactorExtract ) ;
91
+ // Test suite generation
92
+ const newTestCases = await Promise . all ( exportProcedures . map ( async proc => await getTestCaseSpec ( docs , proc , workspaceFolder ) ) ) ;
93
+ const newTestSuite = generateTestSuite ( newTestCases ) ;
94
+ const testSuiteAction = CodeAction . create ( `Generate test suite for '${ fileName } '` , CodeActionKind . RefactorExtract ) ;
95
+ testSuiteAction . edit = {
96
+ documentChanges : [
97
+ CreateFile . create ( testFileUri , { ignoreIfExists : true } ) ,
98
+ TextDocumentEdit . create ( { uri : testFileUri , version : null } , [ TextEdit . insert ( Position . create ( 0 , 0 ) , newTestSuite . join ( `\n` ) ) ] )
99
+ ]
100
+ } ;
101
+ codeActions . push ( testSuiteAction ) ;
102
+ }
103
+ }
56
104
57
- refactorAction . edit = {
58
- changes : {
59
- [ 'mynewtest.rpgle' ] : [
60
- TextEdit . insert (
61
- Position . create ( 0 , 0 ) , // Insert at the start of the new test case file
62
- [
63
- `**free` ,
64
- `` ,
65
- `dcl-proc test_${ currentProcedure . name . toLowerCase ( ) } export;` ,
66
- `` ,
67
- `end-proc;`
68
- ] . join ( `\n` )
105
+ return codeActions ;
106
+ }
69
107
70
- )
71
- ]
72
- } ,
73
- } ;
74
108
75
- return refactorAction ;
109
+ function generateTestSuite ( testCaseSpecs : TestCaseSpec [ ] ) {
110
+ const prototypes = testCaseSpecs . map ( tc => tc . prototype . length > 0 ? [ `` , ...tc . prototype ] : tc . prototype ) . flat ( ) ;
111
+ const testCases = testCaseSpecs . map ( tc => tc . testCase . length > 0 ? [ `` , ...tc . testCase ] : tc . testCase ) . flat ( ) ;
112
+ const allIncludes = testCaseSpecs . map ( tc => tc . includes ) . flat ( ) ;
113
+ const uniqueIncludes = [ ...new Set ( allIncludes ) ] ;
114
+
115
+ return [
116
+ `**free` ,
117
+ `` ,
118
+ `ctl-opt nomain;` ,
119
+ ...prototypes ,
120
+ `` ,
121
+ `/include qinclude,TESTCASE` ,
122
+ ...uniqueIncludes ,
123
+ ...testCases
124
+ ]
125
+ }
126
+
127
+ async function getTestCaseSpec ( docs : Cache , procedure : Declaration , workspaceFolder : WorkspaceFolder ) : Promise < TestCaseSpec > {
128
+ // Get procedure prototype
129
+ const prototype = await getPrototype ( procedure ) ;
130
+
131
+ // Get inputs
132
+ const inputDecs : string [ ] = [ ] ;
133
+ const inputInits : string [ ] = [ ] ;
134
+ const inputIncludes : string [ ] = [ ] ;
135
+ for ( const subItem of procedure . subItems ) {
136
+ const subItemType = docs . resolveType ( subItem ) ;
137
+
138
+ const subItemDec = getDeclaration ( subItemType , `${ subItem . name } ` ) ;
139
+ inputDecs . push ( ...subItemDec ) ;
140
+
141
+ const subItemInits = getInitializations ( docs , subItemType , `${ subItem . name } ` ) ;
142
+ inputInits . push ( ...subItemInits ) ;
143
+
144
+ const subItemIncludes = getIncludes ( subItemType , workspaceFolder ) ;
145
+ inputIncludes . push ( ...subItemIncludes ) ;
146
+ }
147
+
148
+ // Get return
149
+ const resolvedType = docs . resolveType ( procedure ) ;
150
+ const actualDec = getDeclaration ( resolvedType , 'actual' ) ;
151
+ const expectedDec = getDeclaration ( resolvedType , 'expected' ) ;
152
+ const expectedInits = getInitializations ( docs , resolvedType , 'expected' ) ;
153
+ const returnIncludes = getIncludes ( resolvedType , workspaceFolder ) ;
154
+
155
+ // Get unique includes
156
+ const includes = [ ...new Set ( [ ...inputIncludes , ...returnIncludes ] ) ] ;
157
+
158
+ // Get assertions
159
+ const assertions = getAssertions ( docs , resolvedType , 'expected' , 'actual' ) ;
160
+
161
+ const testCase = [
162
+ `dcl-proc test_${ procedure . name } export;` ,
163
+ ` dcl-pi *n extproc(*dclcase) end-pi;` ,
164
+ `` ,
165
+ ...inputDecs . map ( dec => ` ${ dec } ` ) ,
166
+ ...actualDec . map ( dec => ` ${ dec } ` ) ,
167
+ ...expectedDec . map ( dec => ` ${ dec } ` ) ,
168
+ `` ,
169
+ ` // Input` ,
170
+ ...inputInits . map ( init => ` ${ init } ` ) ,
171
+ `` ,
172
+ ` // Actual results` ,
173
+ ` actual = ${ procedure . name } (${ procedure . subItems . map ( s => s . name ) . join ( ` : ` ) } );` ,
174
+ `` ,
175
+ ` // Expected results` ,
176
+ ...expectedInits . map ( init => ` ${ init } ` ) ,
177
+ `` ,
178
+ ` // Assertions` ,
179
+ ...assertions . map ( assert => ` ${ assert } ` ) ,
180
+ `end-proc;`
181
+ ] ;
182
+
183
+ return {
184
+ prototype,
185
+ testCase,
186
+ includes
187
+ } ;
188
+ }
189
+
190
+ function getDeclaration ( detail : RpgleTypeDetail , name : string ) : string [ ] {
191
+ const declarations : string [ ] = [ ] ;
192
+
193
+ if ( detail ) {
194
+ if ( detail . type ) {
195
+ declarations . push ( `dcl-s ${ name } ${ detail . type . name } ${ detail . type . value ? `(${ detail . type . value } )` : `` } ;` ) ;
196
+ } else if ( detail . reference ) {
197
+ declarations . push ( `dcl-ds ${ name } likeDs(${ detail . reference . name } );` ) ;
198
+ }
76
199
}
200
+
201
+ return declarations ;
202
+ }
203
+
204
+ function getInitializations ( docs : Cache , detail : RpgleTypeDetail , name : string ) : string [ ] {
205
+ const inits : string [ ] = [ ] ;
206
+
207
+ if ( detail ) {
208
+ if ( detail . type ) {
209
+ const defaultValue = getDefaultValue ( detail . type . name ) ;
210
+ inits . push ( `${ name } = ${ defaultValue } ;` ) ;
211
+ } else if ( detail . reference ) {
212
+ for ( const subItem of detail . reference . subItems ) {
213
+ const subItemType = docs . resolveType ( subItem ) ;
214
+ const subItemInits = subItemType ?
215
+ getInitializations ( docs , subItemType , `${ name } .${ subItem . name } ` ) : [ ] ;
216
+ inits . push ( ...subItemInits ) ;
217
+ }
218
+ }
219
+ }
220
+
221
+ return inits ;
222
+ }
223
+
224
+ async function getPrototype ( procedure : Declaration ) : Promise < string [ ] > {
225
+ for ( const reference of procedure . references ) {
226
+ const docs = await parser . getDocs ( reference . uri ) ;
227
+ if ( docs ) {
228
+ const prototype = docs . procedures . some ( proc => proc . name === procedure . name && proc . keyword [ 'EXTPROC' ] )
229
+ if ( prototype ) {
230
+ return [ ] ;
231
+ }
232
+ }
233
+ }
234
+
235
+ return [
236
+ `dcl-pr ${ procedure . name } ${ prettyKeywords ( procedure . keyword , true ) } extproc('${ procedure . name . toLocaleUpperCase ( ) } ');` ,
237
+ ...procedure . subItems . map ( s => ` ${ s . name } ${ prettyKeywords ( s . keyword , true ) } ;` ) ,
238
+ `end-pr;`
239
+ ] ;
240
+ }
241
+
242
+ function getIncludes ( detail : RpgleTypeDetail , workspaceFolder : WorkspaceFolder ) : string [ ] {
243
+ const includes : string [ ] = [ ] ;
244
+
245
+ if ( detail . reference ) {
246
+ const structPath = detail . reference . position . path ;
247
+ if ( workspaceFolder ) {
248
+ const relativePath = asPosix ( path . relative ( workspaceFolder . uri , structPath ) ) ;
249
+ if ( ! includes . includes ( relativePath ) ) {
250
+ includes . push ( `/include '${ relativePath } '` ) ; // TODO: Support members style includes
251
+ }
252
+ }
253
+ }
254
+
255
+ return includes ;
256
+ }
257
+
258
+ function getAssertions ( docs : Cache , detail : RpgleTypeDetail , expected : string , actual : string ) : string [ ] {
259
+ const assertions : string [ ] = [ ] ;
260
+
261
+ if ( detail ) {
262
+ if ( detail . type ) {
263
+ const assertion = getAssertion ( detail . type . name ) ;
264
+ const fieldName = actual . split ( `.` ) . pop ( ) ;
265
+ if ( assertion === `assert` ) {
266
+ assertions . push ( `${ assertion } (${ expected } = ${ actual } ${ fieldName ? ` : '${ fieldName } '` : `` } );` ) ;
267
+ } else {
268
+ assertions . push ( `${ assertion } (${ expected } : ${ actual } ${ fieldName ? ` : '${ fieldName } '` : `` } );` ) ;
269
+ }
270
+ } else if ( detail . reference ) {
271
+ for ( const subItem of detail . reference . subItems ) {
272
+ const subItemType = docs . resolveType ( subItem ) ;
273
+ const subItemAssertions = subItemType ?
274
+ getAssertions ( docs , subItemType , `${ expected } .${ subItem . name } ` , `${ actual } .${ subItem . name } ` ) : [ ] ;
275
+ assertions . push ( ...subItemAssertions ) ;
276
+ }
277
+ }
278
+ }
279
+
280
+ return assertions ;
281
+ }
282
+
283
+ function getDefaultValue ( type : RpgleVariableType ) : string {
284
+ switch ( type ) {
285
+ case `char` :
286
+ case `varchar` :
287
+ return `''` ;
288
+ case `int` :
289
+ case `uns` :
290
+ return `0` ;
291
+ case `packed` :
292
+ case `zoned` :
293
+ return `0.0` ;
294
+ case `ind` :
295
+ return `*off` ;
296
+ case `date` :
297
+ return `%date('0001-01-01' : *iso)` ;
298
+ case `time` :
299
+ return `%time('00.00.00' : *iso)` ;
300
+ case `timestamp` :
301
+ return `%timestamp('0001-01-01-00.00.00.000000' : *iso)` ;
302
+ case `pointer` :
303
+ return `*null` ;
304
+ default :
305
+ return 'unknown' ;
306
+ }
307
+ }
308
+
309
+ function getAssertion ( type : RpgleVariableType ) : string {
310
+ switch ( type ) {
311
+ case `char` :
312
+ case `varchar` :
313
+ return `aEqual` ;
314
+ case `int` :
315
+ case `uns` :
316
+ return `iEqual` ;
317
+ case `packed` :
318
+ case `zoned` :
319
+ return `assert` ;
320
+ case `ind` :
321
+ return `nEqual` ;
322
+ case `date` :
323
+ return `assert` ;
324
+ case `time` :
325
+ return `assert` ;
326
+ case `timestamp` :
327
+ return `assert` ;
328
+ case `pointer` :
329
+ return `assert` ;
330
+ default :
331
+ return 'unknown' ;
332
+ }
333
+ }
334
+
335
+ function asPosix ( inPath ?: string ) {
336
+ return inPath ? inPath . split ( path . sep ) . join ( path . posix . sep ) : `` ;
77
337
}
78
338
79
339
function lineAt ( document : TextDocument , line : number ) : string {
0 commit comments