@suffolklitlab/alkiln 5.13.4 → 5.15.0
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/.github/workflows/github_server.yml +1 -1
- package/.github/workflows/playground.yml +2 -1
- package/.github/workflows/unit_tests.yml +1 -1
- package/CHANGELOG.md +40 -0
- package/action.yml +26 -10
- package/action_for_github_server/action.yml +31 -27
- package/docs/4_environments.md +2 -2
- package/docs/artifacts_folder_name.md +121 -71
- package/docs/constrained_random_tests.md +220 -289
- package/docs/decisions/move_runtime_config_to_sources_folder.md +3 -1
- package/docs/prioritize_constrained_random_tests.md +35 -76
- package/lib/docassemble/server_install.js +0 -2
- package/lib/docassemble/setup.js +1 -3
- package/lib/docassemble/takedown.js +2 -2
- package/lib/globals.js +10 -0
- package/lib/run_cucumber.js +40 -53
- package/lib/scope.js +44 -1
- package/lib/setup/TestGenerator.js +1757 -0
- package/lib/setup/artifacts.js +20 -0
- package/lib/setup/generate.js +42 -0
- package/lib/setup/get_Gherkin_AST.js +26 -0
- package/lib/setup/validate.js +89 -0
- package/lib/steps.js +101 -42
- package/lib/utils/Log.js +317 -165
- package/lib/utils/_alkiln-constrained_random_tests_old.js +1776 -0
- package/lib/utils/get_error_stacks.js +16 -0
- package/lib/utils/reports.js +44 -22
- package/lib/utils/session_vars.js +77 -57
- package/lib/utils/set_sources_paths.js +98 -0
- package/package.json +18 -7
- package/tests/log_codes/check_codes.sh +17 -13
- package/tests/log_codes/log_code_expected_instances.txt +58 -0
- package/tests/unit_tests/constrained_random_answers/invalid.fixtures.js +178 -0
- package/tests/unit_tests/constrained_random_answers/invalid.test.js +251 -0
- package/tests/unit_tests/constrained_random_answers/valid.fixtures.js +957 -0
- package/tests/unit_tests/constrained_random_answers/valid.test.js +563 -0
- package/tests/unit_tests/example_of_chai_test_lifecycle.js +78 -0
- package/tests/unit_tests/log.test.js +17 -41
- package/docs/seprate_file_generation_GitHub_action.md +0 -11
- package/lib/parse.js +0 -36
- package/lib/utils/get_sources_paths.js +0 -162
- package/lib/utils/parse.js +0 -1
- package/lib/utils/proccess_command_line_args.js +0 -267
- /package/docs/{expected_inernal_results.md → expected_internal_results.md} +0 -0
|
@@ -0,0 +1,1776 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
*
|
|
4
|
+
* | var | possible_values |
|
|
5
|
+
* | favorite_color | blue;; green;; red |
|
|
6
|
+
*
|
|
7
|
+
* @param raw_var_data { [{ var, possible_values }] } List of table row objects
|
|
8
|
+
* @param raw_var_data[i].var {str} Name of the variable to set
|
|
9
|
+
* @param raw_var_data[i].possible_values {str} Format is ";;" separated
|
|
10
|
+
* possible values for the variable (lawyers use ";" grammatically)
|
|
11
|
+
* */
|
|
12
|
+
|
|
13
|
+
/** Need a sentence for:
|
|
14
|
+
* And I will go to the interview at the address ("")
|
|
15
|
+
* And I will run (100) constrained random tests
|
|
16
|
+
* And those tests should get to one of these page ids - [("lsdjf", "sldkjf")]
|
|
17
|
+
* And I will get there with random choices from this data:
|
|
18
|
+
* | var | possible_values |
|
|
19
|
+
* | favorite_color | blue;; green;; red |
|
|
20
|
+
*
|
|
21
|
+
* Write files
|
|
22
|
+
* 1. Make an object from the given constraints table (split by single _and_
|
|
23
|
+
* double quotes? Just double quotes? Document author _must_ use double
|
|
24
|
+
* quotes?)
|
|
25
|
+
* 1. Make sure that it's not the same as previous objects
|
|
26
|
+
* 1. Far in the future: Make sure it's not the same as a test the author
|
|
27
|
+
* already wrote (need to wrap cucumber for this to run all random tests
|
|
28
|
+
* last)
|
|
29
|
+
* 1. Make a Scenario description using the iteration number
|
|
30
|
+
* 1. Add to one big file (name includes datetime) for all these Scenarios
|
|
31
|
+
* 1. Increment iteration number
|
|
32
|
+
* 1. Leave them there to get run later
|
|
33
|
+
*
|
|
34
|
+
* Supposedly more performant to write one big file [^citation needed]. Not
|
|
35
|
+
* sure it matters that much yet. If we do want to optimize, maybe using
|
|
36
|
+
* a write stream directly is the way to go.
|
|
37
|
+
*
|
|
38
|
+
* Command:
|
|
39
|
+
* I run (\d*) random tests for/at "interview.yml" that will reach one of these question/page ids (\[".*"]) with these constraints: (table)
|
|
40
|
+
* (\d*) random at "interview.yml" will reach one/any of (\[.*]) with: (table)
|
|
41
|
+
*
|
|
42
|
+
* Given I generate 100 random tests
|
|
43
|
+
* And I test the interview at ""
|
|
44
|
+
* And I reach any of these question/page ids: ""
|
|
45
|
+
* And I answer with/give these possible answers: (table)
|
|
46
|
+
* I generate 100 random tests for the interview at "" to reach any of the question ids [] with these possible answers
|
|
47
|
+
*
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* If not doing a 1-Scenario requirement
|
|
52
|
+
*
|
|
53
|
+
* Find end of previous Scenario by going backwards in the list of file lines
|
|
54
|
+
* until we reach a non-comment/blank line.
|
|
55
|
+
* */
|
|
56
|
+
|
|
57
|
+
/** What do I want this AST to look like?
|
|
58
|
+
* Gherkin AST does validating
|
|
59
|
+
*
|
|
60
|
+
* {
|
|
61
|
+
* *
|
|
62
|
+
* feature tags str,
|
|
63
|
+
* *
|
|
64
|
+
* feature str,
|
|
65
|
+
* *
|
|
66
|
+
* scenarios: [
|
|
67
|
+
* scenario1 tags str,
|
|
68
|
+
* *
|
|
69
|
+
* scenario2 str
|
|
70
|
+
* *
|
|
71
|
+
* steps: [
|
|
72
|
+
* *
|
|
73
|
+
* first step 1,
|
|
74
|
+
* *
|
|
75
|
+
* first step 2,
|
|
76
|
+
* *
|
|
77
|
+
* constraints step 1 {
|
|
78
|
+
* ids, table
|
|
79
|
+
* step str,
|
|
80
|
+
* table str
|
|
81
|
+
* (or whole thing)
|
|
82
|
+
* (gherkin AST parser for fake file to get the table. what then?)
|
|
83
|
+
* }
|
|
84
|
+
* *
|
|
85
|
+
* constraints step 2 {}
|
|
86
|
+
* *
|
|
87
|
+
* ]
|
|
88
|
+
* *
|
|
89
|
+
* scenario 2...
|
|
90
|
+
* ]
|
|
91
|
+
* }
|
|
92
|
+
*
|
|
93
|
+
* # or empty line = maybe_scenario_end
|
|
94
|
+
* unless we hit
|
|
95
|
+
* Not something else that is not "Scenario:" (or tags) and `scenario end` is canceled
|
|
96
|
+
* if "Scenario:" (or tags)
|
|
97
|
+
* From the previous maybe_scenario_end and here is the tween scenarios part
|
|
98
|
+
*
|
|
99
|
+
* Except! If we're in quotes... how? What can be delimiters? """ or ```
|
|
100
|
+
*
|
|
101
|
+
*
|
|
102
|
+
* */
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
const COMMENT_REGEX = /^[\s\t]*#/;
|
|
106
|
+
const BLANK_LINE_REGEX = /^[\s\t]*$/;
|
|
107
|
+
|
|
108
|
+
const ALKILN_REGEX = /ALKiln/i;
|
|
109
|
+
const CONSTRAIN_REGEX = /constrain/i; // constrained/constraint/constraints
|
|
110
|
+
const RAND_REGEX = /rand/i; // random/randomness/randomized
|
|
111
|
+
const GENERAT_REGEX = /generat/i; // generate/generates/generated/generating
|
|
112
|
+
const MAKE_REGEX = /make|made/i; // make/makes/made/making
|
|
113
|
+
const NUMBER_REGEX = /\d+/i;
|
|
114
|
+
const CAPTURE_NUMBERS_REGEX = /(\d+)/g;
|
|
115
|
+
const DOULBE_QUOTES_REGEX = /"[^"]+"/;
|
|
116
|
+
const SINGLE_QUOTES_REGEX = /'[^']+'/;
|
|
117
|
+
|
|
118
|
+
class Parser {
|
|
119
|
+
/** step_tweens (between significant steps - fore_step, constraints_step)
|
|
120
|
+
*
|
|
121
|
+
* Need: current type/mode/context and the current line
|
|
122
|
+
*
|
|
123
|
+
* What's the difference between the text of a step and the stuff that comes
|
|
124
|
+
* after like a docstring or datatable
|
|
125
|
+
*
|
|
126
|
+
* modes:
|
|
127
|
+
* - fore_step
|
|
128
|
+
* - constraints_step (do we add tween steps to the top? as long as we don't
|
|
129
|
+
* want to use the gherkin ast parser)
|
|
130
|
+
* - constraints_table
|
|
131
|
+
* - x table_tween
|
|
132
|
+
* - doc_string
|
|
133
|
+
* - doc_string_end ("""/```)
|
|
134
|
+
* - x step_tween
|
|
135
|
+
* - non_mode (everything is a tween unless it starts a new mode)
|
|
136
|
+
*
|
|
137
|
+
* */
|
|
138
|
+
increment(return_obj, index) { return_obj.next = index + 1; return return_obj; }
|
|
139
|
+
|
|
140
|
+
comment({ old_lines=[], index=0, new_lines=[] }) {
|
|
141
|
+
/** Add line */
|
|
142
|
+
let result = { match: false, error: null, lines: new_lines };
|
|
143
|
+
if ( COMMENT_REGEX.test(old_lines[ index ]) ) {
|
|
144
|
+
result.match = true;
|
|
145
|
+
new_lines.push(old_lines[ index ]);
|
|
146
|
+
}
|
|
147
|
+
return this.increment( result, index );
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
blank({ old_lines=[], index=0, new_lines=[] }) {
|
|
151
|
+
/** */
|
|
152
|
+
let result = { match: false, error: null, lines: new_lines };
|
|
153
|
+
if ( BLANK_LINE_REGEX.test(old_lines[ index ]) ) {
|
|
154
|
+
result.match = true;
|
|
155
|
+
new_lines.push(old_lines[ index ]);
|
|
156
|
+
}
|
|
157
|
+
return this.increment( result, index );
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
description({ old_lines=[], index=0, new_lines=[], contexts={} }) {
|
|
161
|
+
/** Do we need this? Should we just outlaw descriptions? */
|
|
162
|
+
if ( !context.scenario ) {
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
num_tests_Step({ old_lines=[], index=0, new_lines=[] }) {
|
|
167
|
+
let result = { match: false, error: null, lines: new_lines };
|
|
168
|
+
let text = old_lines[ index ];
|
|
169
|
+
|
|
170
|
+
// Use `.test()` without `g` flag. See
|
|
171
|
+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test#using_test_on_a_regex_with_the_global_flag
|
|
172
|
+
let is_num_Step = NUMBER_REGEX.test( text )
|
|
173
|
+
&& ALKILN_REGEX.test( text )
|
|
174
|
+
&& CONSTRAIN_REGEX.test( text )
|
|
175
|
+
&& ( RAND_REGEX.test( text )
|
|
176
|
+
|| GENERAT_REGEX.test( text )
|
|
177
|
+
|| MAKE_REGEX.test( text )
|
|
178
|
+
);
|
|
179
|
+
if ( is_num_Step ) {
|
|
180
|
+
result.match = true;
|
|
181
|
+
results.value = { num_to_make: text.match( CAPTURE_NUMBERS_REGEX ) };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return this.increment( result, index );
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
constraints_Step({ old_lines=[], index=0 }) {
|
|
188
|
+
let result = { match: false, error: null };
|
|
189
|
+
let text = old_lines[ index ];
|
|
190
|
+
let step_matches = ALKILN_REGEX.test( text )
|
|
191
|
+
&& DOULBE_QUOTES_REGEX.test( text )
|
|
192
|
+
&& SINGLE_QUOTES_REGEX.test( text );
|
|
193
|
+
if ( !step_matches ) { return result; }
|
|
194
|
+
|
|
195
|
+
let table_lines = old_lines.split( index + 1 );
|
|
196
|
+
for ( let line of table_lines ) {
|
|
197
|
+
if ( this.table({ line }) ) {
|
|
198
|
+
result.matches = true;
|
|
199
|
+
result.value = { line: text }; // Maybe get ids here
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
// Need to change signatures
|
|
203
|
+
if ( this.comment({ line }) || this.blank({ line }) ) {
|
|
204
|
+
continue;
|
|
205
|
+
} else {
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return this.increment( result, index );
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
table({ line, index=0 }) {
|
|
214
|
+
let result = { match: false, error: null };
|
|
215
|
+
if ( /^[\s\t]*\|/.test( line )) {
|
|
216
|
+
result.match = true;
|
|
217
|
+
result.value = { line: line }; // modify line to change header or choose unique value
|
|
218
|
+
}
|
|
219
|
+
return this.increment( result, index );
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
other({ old_lines=[], index=0, new_lines=[] }) {
|
|
223
|
+
/**
|
|
224
|
+
* When in between feature and scenario keyword or between scenario keyword
|
|
225
|
+
* and steps, this is a description */
|
|
226
|
+
let result = { match: false, error: null, lines: new_lines };
|
|
227
|
+
new_lines.push(old_lines[ index ]);
|
|
228
|
+
return this.increment( result, index );
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
run( input_lines ) {
|
|
232
|
+
// let result = this.char( input );
|
|
233
|
+
let ast = {};
|
|
234
|
+
// let starting_function = {
|
|
235
|
+
// constraints, ...
|
|
236
|
+
// }
|
|
237
|
+
let mode_functions = {
|
|
238
|
+
constraints: {
|
|
239
|
+
is_start: function () {
|
|
240
|
+
// ???? Not associated with the mode you're in, but is associated with modes
|
|
241
|
+
},
|
|
242
|
+
is_tween: function () {
|
|
243
|
+
return !input.startsWith(`|`) && !input.startsWith(/[A-z]/); // #
|
|
244
|
+
},
|
|
245
|
+
is_end: function () {
|
|
246
|
+
return input.startsWith(/[A-z]/);
|
|
247
|
+
}
|
|
248
|
+
// just the below?
|
|
249
|
+
is_mode: function ( line, current_mode ) {
|
|
250
|
+
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
constraints_table: {
|
|
254
|
+
is_start: function () {
|
|
255
|
+
if ( current_mode === `constraints` ) {
|
|
256
|
+
return input.startsWith(`|`)...
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
let current_ending_function = null;
|
|
262
|
+
let tweens = [];
|
|
263
|
+
for ( let line of input_lines ) { // is this repeat_until?
|
|
264
|
+
|
|
265
|
+
let { location: { start, end }, mode, is_a_tween, str, ...stoof } = this.parse_one_line( line, this.char, this.something );
|
|
266
|
+
// is pre_end also tween? Leave as empty array?
|
|
267
|
+
if ( is_a_tween ) {
|
|
268
|
+
tweens.push(line);
|
|
269
|
+
} else if ( tweens.length > 0 ) {
|
|
270
|
+
if ( !is_new_mode ) {
|
|
271
|
+
current_line_list.push( ...tweens );
|
|
272
|
+
} else {
|
|
273
|
+
current_line_list = [ ...tweens, line ];
|
|
274
|
+
}
|
|
275
|
+
tweens = [];
|
|
276
|
+
} else {
|
|
277
|
+
if ( !is_new_mode ) {
|
|
278
|
+
current_line_list.push( line );
|
|
279
|
+
} else {
|
|
280
|
+
current_line_list = [ line ];
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if ( mode === `constraints_step` ) {
|
|
284
|
+
current_ending_function = ending_functions.constraints_end;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Maybe do other stuff
|
|
289
|
+
return result;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
nested_run() {
|
|
293
|
+
/**
|
|
294
|
+
* instead of switching between contexts
|
|
295
|
+
* if enter a new "context" before the end of the previous "context", you
|
|
296
|
+
* go one level deeper - a sub-context. Maybe a little recursive.
|
|
297
|
+
*
|
|
298
|
+
*
|
|
299
|
+
* */
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
composable_run() {
|
|
303
|
+
// ???
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
char( input ) {
|
|
307
|
+
if ( input.startsWith(`(`) ) {
|
|
308
|
+
return {error: null, value: input[0], original: input}
|
|
309
|
+
} else {
|
|
310
|
+
return {error: new Error(`Not a left paren`), value: null, original: input}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
repeat_until( input, to_do, is_end ) {
|
|
315
|
+
let result;
|
|
316
|
+
let done = is_end( input )
|
|
317
|
+
while ( !done ) {
|
|
318
|
+
({ result, done } = to_do(input))
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
let parser = new Parser();
|
|
326
|
+
console.log(parser.run(`(thing`));
|
|
327
|
+
console.log(parser.run(`frog`));
|
|
328
|
+
/** Additional complications
|
|
329
|
+
* 1. Step keywords
|
|
330
|
+
*
|
|
331
|
+
* */
|
|
332
|
+
|
|
333
|
+
/** Options with original parsing to reduce complications
|
|
334
|
+
* 1. Duplicate comments at the end of Scenarios
|
|
335
|
+
* 1. Remove comments :(
|
|
336
|
+
* 1. Only allow 1 Scenario
|
|
337
|
+
* */
|
|
338
|
+
|
|
339
|
+
fixts.made = {};
|
|
340
|
+
fixts.made.arg =
|
|
341
|
+
`@go_places
|
|
342
|
+
Feature: I generate constrained random tests
|
|
343
|
+
|
|
344
|
+
This doesn't do anything
|
|
345
|
+
|
|
346
|
+
I make 5 random answer tests
|
|
347
|
+
|
|
348
|
+
@shop
|
|
349
|
+
Here?
|
|
350
|
+
Scenario: I use minimal text (constrain and made)
|
|
351
|
+
This is maybe a description?
|
|
352
|
+
I make 5 random answer tests
|
|
353
|
+
# neither does this
|
|
354
|
+
Given 1 made ALKiln constrain
|
|
355
|
+
And I start the interview at "test_kickout"
|
|
356
|
+
And 1 made "end" constrain
|
|
357
|
+
| var | possible_values |
|
|
358
|
+
# blah
|
|
359
|
+
|
|
360
|
+
| user_choice | correct;; wrong |
|
|
361
|
+
Then I reach "kickout" with:
|
|
362
|
+
| var | possible_values |
|
|
363
|
+
| stoof | ye |
|
|
364
|
+
Then I get to "blah" with this data:
|
|
365
|
+
| var | value |
|
|
366
|
+
| foo | bar |
|
|
367
|
+
|
|
368
|
+
Description here too?`;
|
|
369
|
+
fixts.made.expected =
|
|
370
|
+
`@alkiln @generated @randomized @constraints
|
|
371
|
+
Feature: I generate constrained random tests (generated by ALKiln)
|
|
372
|
+
|
|
373
|
+
@generator1_scenario1_of_3
|
|
374
|
+
Scenario: I use minimal text (constrain and made) (1 of 1)
|
|
375
|
+
Given I start the interview at "test_kickout"
|
|
376
|
+
And I get to any of the question ids ["end"] with this data:
|
|
377
|
+
| var | value |
|
|
378
|
+
${ fixts.RANDOM_TABLE_PLACEHOLDER }
|
|
379
|
+
|
|
380
|
+
@generator1_scenario2_of_3
|
|
381
|
+
Scenario: I use minimal text (constrain and made) (1 of 1)
|
|
382
|
+
Given I start the interview at "test_kickout"
|
|
383
|
+
And I get to any of the question ids ["end"] with this data:
|
|
384
|
+
| var | value |
|
|
385
|
+
${ fixts.RANDOM_TABLE_PLACEHOLDER }
|
|
386
|
+
|
|
387
|
+
@generator1_scenario3_of_3
|
|
388
|
+
Scenario: I use minimal text (constrain and made) (1 of 1)
|
|
389
|
+
Given I start the interview at "test_kickout"
|
|
390
|
+
And I get to any of the question ids ["end"] with this data:
|
|
391
|
+
| var | value |
|
|
392
|
+
${ fixts.RANDOM_TABLE_PLACEHOLDER }
|
|
393
|
+
`;
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
const fs = require('fs');
|
|
405
|
+
const fg = require(`fast-glob`);
|
|
406
|
+
|
|
407
|
+
const globals = require(`../globals`);
|
|
408
|
+
const session_vars = require('./session_vars');
|
|
409
|
+
const get_Gherkin_AST = require(`./get_Gherkin_AST`);
|
|
410
|
+
const get_error_stacks = require(`./get_error_stacks`);
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* TODO: We might give various levels of categories for warnings. For example,
|
|
414
|
+
* severe warnings (like "will skip the random test") vs. slight warnings
|
|
415
|
+
* (like "missing number in constraints random test")
|
|
416
|
+
* TODO: Save the warnings until the tests actually run so they can show up
|
|
417
|
+
* where other warnings show up. Does that mean making a "message to author"
|
|
418
|
+
* internal Step of some kind?
|
|
419
|
+
* TODO: Post-MVP, don't repeat tests that the author has already created
|
|
420
|
+
* Discuss: Require author to use "Generator Scenario:" as they keyword (Also
|
|
421
|
+
* "Generator Scenario Outline:") (or "Scenario Generator", maybe harder to
|
|
422
|
+
* scan) for 2 reasons. 1. Gets the vocab of "generator" in early (though
|
|
423
|
+
* that doesn't match "constraints" Step, which is a whole other language
|
|
424
|
+
* problem we haven't solved yet.) 2. Saying "MAX_RANDOM_TESTS_PER_SCENARIO"
|
|
425
|
+
* is confusing when "Scenario" means different things - a generator Scenario
|
|
426
|
+
* and a regular Scenario.
|
|
427
|
+
* Discuss: Now that Log is less fragile and duplicated, shall we create a new
|
|
428
|
+
* Log instance in here?
|
|
429
|
+
* Discuss: consider debug types that are for authors. Sort of like warnings,
|
|
430
|
+
* but less loud.
|
|
431
|
+
* Discuss: Log.error that just returns a string formatted like an error with
|
|
432
|
+
* the right icon, etc, without logging it to the console. We can _sort_ of
|
|
433
|
+
* do that now, but without the icon.
|
|
434
|
+
* */
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
/** Note: Keep this file non-executable so we can test it more easily. */
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
module.exports = constrained_random_tests = {};
|
|
441
|
+
|
|
442
|
+
let log = null;
|
|
443
|
+
|
|
444
|
+
const FOLDER_NAME = globals.generated_files_folder_name;
|
|
445
|
+
constrained_random_tests.create = function({
|
|
446
|
+
sources_path,
|
|
447
|
+
logger
|
|
448
|
+
}) {
|
|
449
|
+
/** Given files with a certain prefix, use the text in them to generate a
|
|
450
|
+
* feature file or files. A file generates one file with multiple
|
|
451
|
+
* Scenarios.
|
|
452
|
+
*
|
|
453
|
+
* @param {object} obj - Named arguments
|
|
454
|
+
* @param {string} obj.sources_path - Path to the docassemble Sources folder
|
|
455
|
+
*
|
|
456
|
+
* @returns {object} rtn - Named return values
|
|
457
|
+
* @returns {[str]} rtn.new_strings - 0 or more strings that each created a
|
|
458
|
+
* new file.
|
|
459
|
+
* @returns {[Error]} rtn.errors - 0 or more Errors.
|
|
460
|
+
*
|
|
461
|
+
* Details:
|
|
462
|
+
* - Copy sections of text from the old file
|
|
463
|
+
* - Use the Gherkin parser's AST of the constraints Step in each Scenario to
|
|
464
|
+
* generate Story Tables
|
|
465
|
+
* - Combine various sections with the Story Tables
|
|
466
|
+
* - Make a new file with that content
|
|
467
|
+
*
|
|
468
|
+
* We're trying to find the least complex, yet sufficiently robust, system
|
|
469
|
+
*
|
|
470
|
+
* Goal:
|
|
471
|
+
* Stay as true to the author's syntax as possible so they can get useful
|
|
472
|
+
* errors from cucumber itself.
|
|
473
|
+
*
|
|
474
|
+
* Anti-goals:
|
|
475
|
+
* - Be cucumber
|
|
476
|
+
* - Render the whole AST ourselves
|
|
477
|
+
*
|
|
478
|
+
* For examples, see tests.
|
|
479
|
+
*
|
|
480
|
+
* */
|
|
481
|
+
log = logger;
|
|
482
|
+
|
|
483
|
+
let new_files_strings = [];
|
|
484
|
+
let all_errors = [];
|
|
485
|
+
|
|
486
|
+
// Get the paths of the source files that we need to parse
|
|
487
|
+
let generator_paths = fg.sync(`${ sources_path }/${ globals.generators_prefix }*.feature`);
|
|
488
|
+
for ( let generator_path of generator_paths ) {
|
|
489
|
+
|
|
490
|
+
let file_text = fs.readFileSync( generator_path, {encoding: `utf8`});
|
|
491
|
+
|
|
492
|
+
log.info({ code: `ALK0231`, context: `generating files`, },
|
|
493
|
+
`Starting to generate the constrained random answers tests for "${ generator_path }"`
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
let { new_contents, errors: parse_errors } = constrained_random_tests.parse_file({
|
|
497
|
+
file_text,
|
|
498
|
+
generator_path
|
|
499
|
+
});
|
|
500
|
+
all_errors.push( ...parse_errors );
|
|
501
|
+
|
|
502
|
+
if ( new_contents ) {
|
|
503
|
+
new_files_strings.push( new_contents );
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
let tests_path = `${ sources_path }/${ FOLDER_NAME }`;
|
|
508
|
+
let artifacts_path = `${ session_vars.get_artifacts_path_name() }/${ FOLDER_NAME }`;
|
|
509
|
+
save_files({ tests_path, artifacts_path, file_strings: new_files_strings });
|
|
510
|
+
|
|
511
|
+
return { new_strings: new_files_strings, errors: all_errors };
|
|
512
|
+
}; // Ends make_random_input_files()
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
constrained_random_tests.parse_file = function({ file_text, generator_path, logger }) {
|
|
516
|
+
/** Parse and return one file's contents
|
|
517
|
+
*
|
|
518
|
+
* TODO: If there's a Gherkin parser error, log something more clear before
|
|
519
|
+
* re-throwing it?
|
|
520
|
+
*
|
|
521
|
+
* Note: The generator file should be valid by the time it gets here.
|
|
522
|
+
*
|
|
523
|
+
* @param {string} file_text - Full text of a generator feature file
|
|
524
|
+
*
|
|
525
|
+
* @returns {object} rtn - Named return values
|
|
526
|
+
* @returns {null | str} rtn.new_contents - `null` or string for the new file
|
|
527
|
+
* @returns {[Error]} rtn.errors - 0 or more Errors.
|
|
528
|
+
* */
|
|
529
|
+
// Unit tests call the parser alone, so they need to create their own logger.
|
|
530
|
+
if ( logger ) {
|
|
531
|
+
log = logger;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
let errors = [];
|
|
535
|
+
|
|
536
|
+
let { doc_AST, errors: generator_Gherkin_errors } = get_generator_AST({ file_text, generator_path });
|
|
537
|
+
if ( generator_Gherkin_errors.length > 0 ) {
|
|
538
|
+
return { new_contents: null, errors: generator_Gherkin_errors };
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
let { new_contents, errors: generating_errors } = get_new_file_contents({ doc_AST, file_text, generator_path });
|
|
542
|
+
if ( generating_errors.length > 0 ) {
|
|
543
|
+
return { new_contents: null, errors: generating_errors };
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
let { errors: generated_parsing_errors } = validate_syntax({ file_text: new_contents, generator_path });
|
|
547
|
+
if ( generated_parsing_errors.length > 0 ) {
|
|
548
|
+
let syntax_error = log.warn({ code: `ALK0249`, context: `generating files`, before: `\n\n`, },
|
|
549
|
+
`The .feature file ALKiln generated from "${ generator_path }" had Gherkin syntax error(s)\n`,
|
|
550
|
+
...get_error_stacks({ errors: generated_parsing_errors })
|
|
551
|
+
);
|
|
552
|
+
return { new_contents, errors: [ new Error( syntax_error )] };
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
return { new_contents, errors };
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
function get_generator_AST({ file_text, generator_path }) {
|
|
560
|
+
/** Return any errors created when Gherkin tries to parse the generator file.
|
|
561
|
+
* If there are any errors, throw a list of all the errors.
|
|
562
|
+
*
|
|
563
|
+
* @param {object} obj - Named arguments
|
|
564
|
+
* @param {string} obj.file_text - Text of the Gherkin file
|
|
565
|
+
* @param {string} obj.generator_path - Path to the generator file
|
|
566
|
+
*
|
|
567
|
+
* @returns {object} rtn - Named return values
|
|
568
|
+
* @returns {object} rtn.doc_AST - Gherkin AST of a .feature file
|
|
569
|
+
* */
|
|
570
|
+
let { AST: doc_AST, errors: generator_errors } = get_Gherkin_AST({ file_text });
|
|
571
|
+
|
|
572
|
+
if ( generator_errors.length <= 0 && doc_AST.feature ) {
|
|
573
|
+
return { doc_AST, errors: [] };
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/** Error handling */
|
|
577
|
+
if ( generator_errors.length > 0 ) {
|
|
578
|
+
let syntax_error = log.warn({ code: `ALK0236`, context: `generating files`, before: `\n\n`, },
|
|
579
|
+
`The author's generator .feature file at "${ generator_path }" Gherkin syntax error(s). You might see more warnings above or below this one.\n`,
|
|
580
|
+
...get_error_stacks({ errors: generator_errors })
|
|
581
|
+
);
|
|
582
|
+
return { doc_AST: null, errors: [ new Error( syntax_error )] };
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// We have never been able to trigger `!doc_AST`
|
|
586
|
+
|
|
587
|
+
if ( !doc_AST.feature ) {
|
|
588
|
+
let feature_error = log.warn({ code: `ALK0254`, context: `generating files`, before: `\n\n`, },
|
|
589
|
+
`The author's generator .feature file at "${ generator_path }" needs to include the \`Feature\` keyword.\n`
|
|
590
|
+
);
|
|
591
|
+
return { doc_AST: null, errors: [ new Error( feature_error )] };
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Should not get here
|
|
595
|
+
let unknown_error = log.debug({ code: `ALK0255`, context: `generating files`, before: `\n\n`, },
|
|
596
|
+
`The author's generator .feature file at "${ generator_path }" ran into an unknown error when ALKiln tried to parse it.\n`
|
|
597
|
+
);
|
|
598
|
+
return { doc_AST, errors: [ new Error( unknown_error )] };
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
function validate_syntax({ file_text }) {
|
|
603
|
+
/** Return any errors we get from running the text through the Gherkin parser
|
|
604
|
+
*
|
|
605
|
+
* @param {object} obj - Named arguments
|
|
606
|
+
* @param {str} obj.file_text - Text of the Gherkin file
|
|
607
|
+
*
|
|
608
|
+
* @returns {object} rtn - Named return values
|
|
609
|
+
* @returns {[Error]} rtn.errors - 0 or more Errors.
|
|
610
|
+
* */
|
|
611
|
+
let { errors: generated_parsing_errors } = get_Gherkin_AST({ file_text });
|
|
612
|
+
return { errors: generated_parsing_errors || [] };
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
function save_files({ tests_path, artifacts_path, file_strings }) {
|
|
617
|
+
/** Save each test file string to an artifacts folder and a folder that
|
|
618
|
+
* cucumber will use to run tests.
|
|
619
|
+
*
|
|
620
|
+
* @param { object } obj - Named arguments
|
|
621
|
+
* @param { str } tests_path - Path where cucumberjs will find these files
|
|
622
|
+
* @param { str } artifacts_path - Artifacts folder path
|
|
623
|
+
* @param { [str] } file_strings - Each string will be a test file
|
|
624
|
+
*
|
|
625
|
+
* @returns { undefined }
|
|
626
|
+
* */
|
|
627
|
+
// Delete the old files no matter what
|
|
628
|
+
delete_previous_tests_from_sources_folders_for_ALKiln_developers({ tests_path });
|
|
629
|
+
|
|
630
|
+
if ( file_strings.length === 0 ) { return; }
|
|
631
|
+
|
|
632
|
+
// Implementation note: This has no error if the folder already exists
|
|
633
|
+
fs.mkdirSync( tests_path, { recursive: true });
|
|
634
|
+
fs.mkdirSync( artifacts_path, { recursive: true });
|
|
635
|
+
|
|
636
|
+
// TODO: For internal testing, create a separate GitHub workflow to test the
|
|
637
|
+
// number of files that were generated
|
|
638
|
+
log.debug({ code: `ALK0250`, context: `generating files`, },
|
|
639
|
+
`Saving ${ file_strings.length } newly generated files in "${ tests_path }"`
|
|
640
|
+
);
|
|
641
|
+
|
|
642
|
+
let count = 1;
|
|
643
|
+
for ( let file_str of file_strings ) {
|
|
644
|
+
let filename = `${ count }_${ Date.now() }.feature`;
|
|
645
|
+
count++;
|
|
646
|
+
// Implementation note: If the files don't exist, this creates them
|
|
647
|
+
fs.appendFileSync( `${ tests_path }/${ filename }`, file_str );
|
|
648
|
+
fs.appendFileSync( `${ artifacts_path }/${ filename }`, file_str );
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
log.debug({ code: `ALK0252`, context: `generating files`, },
|
|
652
|
+
`Saved new generated files:`, fs.readdirSync( tests_path )
|
|
653
|
+
);
|
|
654
|
+
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
function delete_previous_tests_from_sources_folders_for_ALKiln_developers({ tests_path }) {
|
|
659
|
+
/** Delete previously generated files from this folder if they exist because
|
|
660
|
+
* otherwise those tests will run again too (like for ALKiln devs).
|
|
661
|
+
*
|
|
662
|
+
* Note: All the relevant tests that cucumber will run will be on the paths
|
|
663
|
+
* that come in here, so all *relevant* old files will get deleted, even if
|
|
664
|
+
* there are previous files hanging around somewhere else.
|
|
665
|
+
*
|
|
666
|
+
* Research: How will this folder behave on the Playground? Do we need to
|
|
667
|
+
* clean up at the end too?
|
|
668
|
+
* */
|
|
669
|
+
if ( !fs.existsSync( tests_path )) { return; }
|
|
670
|
+
log.debug({ code: `ALK0251`, context: `generating files`, },
|
|
671
|
+
`Deleting previously generated files:`, fs.readdirSync( tests_path )
|
|
672
|
+
);
|
|
673
|
+
fs.rmSync( tests_path, { recursive: true, force: true });
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
function get_new_file_contents({ doc_AST={}, file_text=``, generator_path }) {
|
|
678
|
+
/** Return the text for one new file with tests based on one generator file.
|
|
679
|
+
*
|
|
680
|
+
* Note: Right now it is out of scope to save comments in constraints Steps.
|
|
681
|
+
*
|
|
682
|
+
* @param {object} obj - Named arguments
|
|
683
|
+
* @param {object} obj.doc_AST - Gherkin-parsed abstract syntax tree of
|
|
684
|
+
* generator file
|
|
685
|
+
* @param {string} obj.file_text - String of the whole generator file
|
|
686
|
+
* @param {string} obj.generator_path - Path to the generator file
|
|
687
|
+
*
|
|
688
|
+
* @returns {null | string} - The text (Feature, Scenarios, etc) for the new
|
|
689
|
+
* file
|
|
690
|
+
*
|
|
691
|
+
* @returns {object} rtn - Named return values
|
|
692
|
+
* @returns {null | str} rtn.new_contents - `null` or string for the new file
|
|
693
|
+
* @returns {[Error]} rtn.errors - 0 or more Errors.
|
|
694
|
+
* */
|
|
695
|
+
|
|
696
|
+
// `doc_AST.feature.children` seem to always be defined by here
|
|
697
|
+
let scenario_objs = doc_AST.feature.children;
|
|
698
|
+
if ( !scenario_objs[0] ) {
|
|
699
|
+
let syntax_error = log.warn({ code: `ALK0256`, context: `generating files`, before: `\n\n`, },
|
|
700
|
+
`The author's generator .feature file at "${ generator_path }" may be missing its Scenarios.\n`
|
|
701
|
+
);
|
|
702
|
+
return { new_contents: null, errors: [ new Error( syntax_error ) ]};
|
|
703
|
+
}
|
|
704
|
+
// `scenario_objs[0].scenario` seems to always be defined by here
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
let generation_errors = [];
|
|
708
|
+
|
|
709
|
+
let generator_lines = file_text.split(`\n`);
|
|
710
|
+
|
|
711
|
+
let feature_text = get_Feature_text({
|
|
712
|
+
feature: doc_AST.feature,
|
|
713
|
+
generator_lines
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
let { scenarios_strings, errors: scenarios_errors } = get_Scenarios({
|
|
717
|
+
scenario_objs,
|
|
718
|
+
generator_lines
|
|
719
|
+
});
|
|
720
|
+
generation_errors.push( ...scenarios_errors );
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
let count_msg = `ALKiln generated ${ scenarios_strings.length } Scenarios`;
|
|
724
|
+
|
|
725
|
+
if ( scenarios_strings.length === 0 ) {
|
|
726
|
+
let error = log.warn({ code: `ALK0246`, context: `generating files`, }, count_msg );
|
|
727
|
+
generation_errors.push( new Error( error ));
|
|
728
|
+
return { new_contents: null, errors: generation_errors };
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
log.info({ code: `ALK0247`, context: `generating files`, }, count_msg );
|
|
732
|
+
|
|
733
|
+
let scenarios_combined = scenarios_strings.join(`\n`);
|
|
734
|
+
let new_file_contents = [ feature_text, scenarios_combined ].join(`\n`);
|
|
735
|
+
|
|
736
|
+
if ( !new_file_contents ) {
|
|
737
|
+
let msg = `Unable to generate a constrained random answers file from "${ generator_path }". See above for warnings and errors.`;
|
|
738
|
+
let error = log.warn({ code: `ALK0248`, context: `generating files`, }, msg );
|
|
739
|
+
return { new_contents: null, errors: [ new Error( error )] };
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
return { new_contents: new_file_contents, errors: generation_errors };
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
function get_Feature_text({ feature, generator_lines }) {
|
|
747
|
+
/** Return text of Feature section to put at the top of the new file
|
|
748
|
+
*
|
|
749
|
+
* @param {object} obj - Named arguments
|
|
750
|
+
* @param {object} obj.feature - Gherkin AST `feature` object
|
|
751
|
+
* @param {[string]} obj.generator_lines - Text of the generator file as a
|
|
752
|
+
* list
|
|
753
|
+
*
|
|
754
|
+
* @returns {string} - Text of the new file's Feature section
|
|
755
|
+
* */
|
|
756
|
+
add_Feature_randomization_indicators({ generator_lines, feature });
|
|
757
|
+
|
|
758
|
+
let first_scenario = feature.children[0].scenario;
|
|
759
|
+
let first_Scenario_line = get_Scenario_start_line_num({ scenario: first_scenario });
|
|
760
|
+
let first_Scenario_file_index = as_index( first_Scenario_line );
|
|
761
|
+
let index_of_last_line = first_Scenario_file_index - 1;
|
|
762
|
+
|
|
763
|
+
let feature_text = generator_lines.slice(
|
|
764
|
+
0, index_of_last_line + 1 // `slice` excludes the last index
|
|
765
|
+
).join(`\n`);
|
|
766
|
+
|
|
767
|
+
return feature_text;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
|
|
771
|
+
function get_Scenarios({ scenario_objs, generator_lines }) {
|
|
772
|
+
/** Return all the strings of the generated Scenarios and any Errors we ran
|
|
773
|
+
* into.
|
|
774
|
+
*
|
|
775
|
+
* @param {object} obj - Named arguments
|
|
776
|
+
* @param {object} obj.scenario_objs - Gherkin Scenario objects
|
|
777
|
+
* @param {[str]} obj.generator_lines - List of lines in the generator file
|
|
778
|
+
*
|
|
779
|
+
* Starting here, functions' return values should now include lists of warning
|
|
780
|
+
* messages. If a Scenario gets any warnings, we will add a "warnings"
|
|
781
|
+
* Step to that Scenario. Implement over time.
|
|
782
|
+
*
|
|
783
|
+
* @returns {object} rtn - Named return values
|
|
784
|
+
* @returns {[str]} rtn.scenarios - 0 or more Scenario strings
|
|
785
|
+
* @returns {[Error]} rtn.errors - 0 or more Error objects
|
|
786
|
+
* */
|
|
787
|
+
let errs_n_warns = new Misbehaviors();
|
|
788
|
+
|
|
789
|
+
let all_Scenarios_new_strings = [];
|
|
790
|
+
for ( let scenario_obj_i = 0; scenario_obj_i < scenario_objs.length; scenario_obj_i++ ) {
|
|
791
|
+
|
|
792
|
+
errs_n_warns.reset_warnings();
|
|
793
|
+
|
|
794
|
+
/** Start returning warnings here. */
|
|
795
|
+
|
|
796
|
+
// Get all randomized Scenarios from one generator Scenario
|
|
797
|
+
let generator_Scenario = scenario_objs[ scenario_obj_i ].scenario;
|
|
798
|
+
|
|
799
|
+
// Get the values for the various Steps that have to come before any other
|
|
800
|
+
// Steps. Right now, that's only num_to_generate. Premature? Ask Will.
|
|
801
|
+
let {
|
|
802
|
+
num_to_generate,
|
|
803
|
+
warnings: first_Steps_warnings,
|
|
804
|
+
errors: first_Steps_errors
|
|
805
|
+
} = get_first_Steps({ scenario: generator_Scenario });
|
|
806
|
+
errs_n_warns.add({
|
|
807
|
+
warnings: first_Steps_warnings, errors: first_Steps_errors
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
// // Examples of constraints Steps
|
|
811
|
+
// // And I generate 34 tests that get to any of ["end"] when I pick from these constraints:
|
|
812
|
+
// // And I make 1 constrained random test that get to "end1", "end 2", or "end-3" with:
|
|
813
|
+
// let {
|
|
814
|
+
// constraints_Step,
|
|
815
|
+
// warnings: constraints_warnings,
|
|
816
|
+
// errors: constraints_errors
|
|
817
|
+
// } = get_constraints_Step({ generator_Scenario });
|
|
818
|
+
// errs_n_warns.add({ warnings: constraints_warnings, errors: constraints_errors });
|
|
819
|
+
// if ( errs_n_warns.errors.length > 0 ) { continue; }
|
|
820
|
+
|
|
821
|
+
// // let {
|
|
822
|
+
// // num_to_generate,
|
|
823
|
+
// // warnings: num_warnings,
|
|
824
|
+
// // errors: num_errors
|
|
825
|
+
// // } = get_number_of_Scenarios_to_generate({ step_text: constraints_Step.text });
|
|
826
|
+
// // errs_n_warns.add({ warnings: num_warnings, errors: num_errors });
|
|
827
|
+
|
|
828
|
+
// let {
|
|
829
|
+
// ids: final_page_ids,
|
|
830
|
+
// warning: ids_warnings,
|
|
831
|
+
// errors: ids_errors
|
|
832
|
+
// } = get_final_ids({ step_text: constraints_Step.text });
|
|
833
|
+
// errs_n_warns.add({ warnings: ids_warnings, errors: ids_errors });
|
|
834
|
+
// if ( final_page_ids === null ) { continue; }
|
|
835
|
+
|
|
836
|
+
// let {
|
|
837
|
+
// story_tables: generated_story_tables_strs,
|
|
838
|
+
// warnings: story_tables_warnings,
|
|
839
|
+
// errors: story_tables_errors
|
|
840
|
+
// } = get_unique_Story_Tables_strings({
|
|
841
|
+
// num_to_generate,
|
|
842
|
+
// ids: final_page_ids,
|
|
843
|
+
// step: constraints_Step
|
|
844
|
+
// });
|
|
845
|
+
// errs_n_warns.add({ warnings: story_tables_warnings, errors: story_tables_errors });
|
|
846
|
+
|
|
847
|
+
// /** Stop returning warnings here. */
|
|
848
|
+
|
|
849
|
+
// let { warnings_Step, errors: warning_step_errors } = get_warnings_Step({
|
|
850
|
+
// warnings: errs_n_warns.warnings,
|
|
851
|
+
// step_text: constraints_Step.text
|
|
852
|
+
// });
|
|
853
|
+
// errs_n_warns.add({ errors: warning_step_errors });
|
|
854
|
+
|
|
855
|
+
// // The Scenario top and bottom will wrap every generated Story Table for
|
|
856
|
+
// // this generator Scenario
|
|
857
|
+
// let all_scenarios_top = get_pre_constraints({
|
|
858
|
+
// generator_Scenario,
|
|
859
|
+
// constraints_Step,
|
|
860
|
+
// generator_lines,
|
|
861
|
+
// has_warnings: errs_n_warns.warnings.length > 0
|
|
862
|
+
// });
|
|
863
|
+
// let all_scenarios_bottom = get_post_constraints({
|
|
864
|
+
// next_Scenario: (scenario_objs[ scenario_obj_i + 1 ] || {}).scenario,
|
|
865
|
+
// constraints_Step,
|
|
866
|
+
// generator_lines
|
|
867
|
+
// });
|
|
868
|
+
|
|
869
|
+
// let generated_Scenarios_str = get_generated_Scenarios_str({
|
|
870
|
+
// all_scenarios_top,
|
|
871
|
+
// warnings_Step,
|
|
872
|
+
// generator_id: scenario_obj_i,
|
|
873
|
+
// story_tables: generated_story_tables_strs,
|
|
874
|
+
// all_scenarios_bottom
|
|
875
|
+
// });
|
|
876
|
+
|
|
877
|
+
// all_Scenarios_new_strings.push( generated_Scenarios_str );
|
|
878
|
+
} // ends for every scenario_container
|
|
879
|
+
|
|
880
|
+
// return { scenarios_strings: all_Scenarios_new_strings, errors: errs_n_warns.errors }
|
|
881
|
+
return { scenarios_strings: ['Feature: deving it'], errors: [] };
|
|
882
|
+
} // Ends get_Scenarios()
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
class Misbehaviors {
|
|
886
|
+
/** Accumulates and manages errors and warnings lists. This lets you
|
|
887
|
+
* accumulate errors for a whole function. A loop in that function, though,
|
|
888
|
+
* can build up warnings just for that loop, after which you can reset the
|
|
889
|
+
* warnings list for the next loop. Each loop can add up its own warnings.
|
|
890
|
+
*
|
|
891
|
+
* Example:
|
|
892
|
+
*
|
|
893
|
+
* function do_important_things() {
|
|
894
|
+
* let misfits = new Misbehaviors();
|
|
895
|
+
*
|
|
896
|
+
* let { result, errors } = func();
|
|
897
|
+
* misfits.add({ errors });
|
|
898
|
+
*
|
|
899
|
+
* for ( let item of items ) {
|
|
900
|
+
*
|
|
901
|
+
* misfits.reset_warnings();
|
|
902
|
+
*
|
|
903
|
+
* let { foo, warnings1, errors1 } = func1( item );
|
|
904
|
+
* misfits.add({ warnings: warnings1, errors: errors1 });
|
|
905
|
+
*
|
|
906
|
+
* let { bar, warnings2, errors2 } = func2( item );
|
|
907
|
+
* misfits.add({ warnings: warnings2, errors: errors2 });
|
|
908
|
+
*
|
|
909
|
+
* console.log( misfits.warnings );
|
|
910
|
+
* // Loop 1: [ "oops", "darn" ]
|
|
911
|
+
* // Loop 2: [ "shoot", "zoinks" ]
|
|
912
|
+
* }
|
|
913
|
+
*
|
|
914
|
+
* console.log( misfits.errors );
|
|
915
|
+
* // [ "ahh!", "gorramit", "that's done it" ]
|
|
916
|
+
* }
|
|
917
|
+
* */
|
|
918
|
+
constructor() {
|
|
919
|
+
this.errors = [];
|
|
920
|
+
this.warnings = [];
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
reset_warnings() {
|
|
924
|
+
/** Sets warnings to an empty list. This is useful inside a loop that still
|
|
925
|
+
* needs to keep
|
|
926
|
+
*
|
|
927
|
+
* @returns {undefined}
|
|
928
|
+
* */
|
|
929
|
+
this.warnings = [];
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
add({ errors=[], warnings=[] }) {
|
|
933
|
+
/** Adds errors and/or warnings to their list
|
|
934
|
+
*
|
|
935
|
+
* @param {obj} obj - Named arguments
|
|
936
|
+
* @param {[str]} obj.warnings - (Optional) 0 or more warning messages
|
|
937
|
+
* @param {[Error]} obj.errors - (Optional) 0 or more Error objects
|
|
938
|
+
*
|
|
939
|
+
* @returns {undefined}
|
|
940
|
+
* */
|
|
941
|
+
if ( this.errors ) { this.errors.push( ...errors ); }
|
|
942
|
+
if (this.warnings ) { this.warnings.push( ...warnings ); }
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
|
|
947
|
+
function get_first_Steps({ scenario }) {
|
|
948
|
+
/**
|
|
949
|
+
*
|
|
950
|
+
* Discuss: If a "first" Step appears multiple times, should that be an error
|
|
951
|
+
* or a warning?
|
|
952
|
+
* */
|
|
953
|
+
let misfits = new Misbehaviors();
|
|
954
|
+
|
|
955
|
+
let first_vals = {
|
|
956
|
+
num_to_make: null,
|
|
957
|
+
};
|
|
958
|
+
|
|
959
|
+
// let index_of_first_non_first_Step = null;
|
|
960
|
+
let step_index = -1;
|
|
961
|
+
for ( let step of steps ) {
|
|
962
|
+
|
|
963
|
+
step_index++;
|
|
964
|
+
let all_results = [];
|
|
965
|
+
|
|
966
|
+
/** Number of tests to generate */
|
|
967
|
+
let num_results = get_number_of_Scenarios_to_generate2({
|
|
968
|
+
step,
|
|
969
|
+
prev_num_to_make: first_vals.num_to_make
|
|
970
|
+
});
|
|
971
|
+
misfits.add({ warnings: num_results.warnings, errors: num_results.errors });
|
|
972
|
+
if ( num_results.num_to_make !== null && first_vals.num_to_make !== null ) {
|
|
973
|
+
misfits.add({ warnings: log.warn({ code: `ALK0260`, context: `generating files`, },
|
|
974
|
+
`A generator Scenario has too many Steps that set the number of tests to generate. The Scenario's name:\n${ scenario.name }`
|
|
975
|
+
)
|
|
976
|
+
})
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
first_vals.num_to_make = num_results.num_to_make || first_vals.num_to_make;
|
|
980
|
+
all_results.push( first_vals.num_to_make );
|
|
981
|
+
|
|
982
|
+
/** Other possible first Steps */
|
|
983
|
+
// TBA
|
|
984
|
+
|
|
985
|
+
/** Possibly the end */
|
|
986
|
+
// If none of the "first Step" values were satisfied, stop here
|
|
987
|
+
if ( all_results.every(function ( item ) { return item === null; }) ) {
|
|
988
|
+
break;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
return { num_to_make, final_index: step_index, warnings: misfits.warnings, errors: misfits.errors };
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
|
|
997
|
+
// TODO: Consider adding "pick" and "choose" as alternatives for "constrain"
|
|
998
|
+
const CONSTRAIN_REGEX = /constrain/i; // constrained/constraint/constraints
|
|
999
|
+
const RAND_REGEX = /rand/i; // random/randomness/randomized
|
|
1000
|
+
const GENERAT_REGEX = /generat/i; // generate/generates/generated/generating
|
|
1001
|
+
const MAKE_REGEX = /mak|mad/i; // make/makes/made/making
|
|
1002
|
+
const CAPTURE_NUMBERS_REGEX = /(\d+)/g;
|
|
1003
|
+
function get_number_of_Scenarios_to_generate2({ step }) {
|
|
1004
|
+
/** Return null or a constraints Step.
|
|
1005
|
+
*
|
|
1006
|
+
* @param {object} obj - Named arguments
|
|
1007
|
+
* @param {object} obj.step - A valid constraints Step Gherkin object
|
|
1008
|
+
*
|
|
1009
|
+
* @returns {null | object} `null` or a constraints Step
|
|
1010
|
+
* */
|
|
1011
|
+
|
|
1012
|
+
// And ALKiln's generated/new tests can definitely get to one of "" "" with:
|
|
1013
|
+
// And the author promises that the generated tests can get to one of "" "" with:
|
|
1014
|
+
|
|
1015
|
+
// Given ALKiln should make 10 constrained random tests
|
|
1016
|
+
// mak/mad/generat, \d, constrain, rand, test???
|
|
1017
|
+
let user_num = null;
|
|
1018
|
+
let warnings = [];
|
|
1019
|
+
let errors = [];
|
|
1020
|
+
|
|
1021
|
+
// Use `.test()` without `g` flag. See
|
|
1022
|
+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test#using_test_on_a_regex_with_the_global_flag
|
|
1023
|
+
let is_num_Step = CONSTRAIN_REGEX.test( step.text )
|
|
1024
|
+
&& ( RAND_REGEX.test( step.text )
|
|
1025
|
+
|| GENERAT_REGEX.test( step.text )
|
|
1026
|
+
|| MAKE_REGEX.test( step.text )
|
|
1027
|
+
);
|
|
1028
|
+
|
|
1029
|
+
let number_matcher = step.text.match( CAPTURE_NUMBERS_REGEX );
|
|
1030
|
+
|
|
1031
|
+
/** No numbers */
|
|
1032
|
+
if ( !is_num_Step || number_matcher === null ) {
|
|
1033
|
+
warnings.push(
|
|
1034
|
+
log.warn({ code: `ALK0237`, context: `generating files`, },
|
|
1035
|
+
`A constrained random answers Step is missing the number of tests to generate. ALKiln will create 1 test. The Step:\n${ step.text }`
|
|
1036
|
+
)
|
|
1037
|
+
);
|
|
1038
|
+
return { num_to_generate: null, warnings, errors };
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
/** Too many numbers */
|
|
1042
|
+
if ( number_matcher && number_matcher.length > 1 ) {
|
|
1043
|
+
// Some feedback indicated using the last number makes more sense to people
|
|
1044
|
+
// than the first
|
|
1045
|
+
user_num = parseInt( number_matcher[ number_matcher.length - 1 ]);
|
|
1046
|
+
warnings.push(
|
|
1047
|
+
log.warn({ code: `ALK0238`, context: `generating files`, },
|
|
1048
|
+
`This constrained random answers Step is ambiguous because it has more than 1 number. ALKiln will use the last number the author gave ("${ user_num }"). The Step:\n${ step.text }`,
|
|
1049
|
+
)
|
|
1050
|
+
);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
/** Happy path */
|
|
1054
|
+
if ( number_matcher && !user_num ) {
|
|
1055
|
+
user_num = parseInt( number_matcher[ 0 ]);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
let num_to_generate = Math.min(
|
|
1059
|
+
session_vars.get_max_tests_per_generator_scenario(),
|
|
1060
|
+
user_num
|
|
1061
|
+
);
|
|
1062
|
+
|
|
1063
|
+
if ( num_to_generate !== user_num ) {
|
|
1064
|
+
warnings.push(
|
|
1065
|
+
log.warn({ code: `ALK0253`, context: `generating files`, },
|
|
1066
|
+
`The author asked to generate ${ user_num } tests, but maximum allowed is ${ num_to_generate }. If you want to increase the maximum, use the MAX_TESTS_PER_GENERATOR_SCENARIO environment variable. Read more about environment variables at https://assemblyline.suffolklitlab.org/docs/alkiln/writing/#env-vars.`
|
|
1067
|
+
)
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
return { num_to_generate, warnings, errors };
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
|
|
1075
|
+
function add_Feature_randomization_indicators({ generator_lines=``, feature={} }) {
|
|
1076
|
+
/** Mutates `generator_lines`. Modifies the tag and name of the Feature section.
|
|
1077
|
+
*
|
|
1078
|
+
* @param {object} obj - Named arguments
|
|
1079
|
+
* @param {object} obj.feature - Gherkin AST `feature` object
|
|
1080
|
+
* @param {[string]} obj.generator_lines - Text of the generator file as a list
|
|
1081
|
+
*
|
|
1082
|
+
* @returns {undefined}
|
|
1083
|
+
* */
|
|
1084
|
+
let feature_index = as_index( feature.location.line );
|
|
1085
|
+
|
|
1086
|
+
let tag_indicator = `@alkiln @generated @randomized @constraints`;
|
|
1087
|
+
let name_indicator = `(generated by ALKiln)`;
|
|
1088
|
+
let new_f_name_line = ``;
|
|
1089
|
+
|
|
1090
|
+
if ( feature.tags.length > 0 ) {
|
|
1091
|
+
// Modify existing tags
|
|
1092
|
+
let tags_index = as_index( feature.tags[0].location.line );
|
|
1093
|
+
let tags_starter = generator_lines[ tags_index ];
|
|
1094
|
+
|
|
1095
|
+
generator_lines[ tags_index ] = `${ tags_starter } ${ tag_indicator }`;
|
|
1096
|
+
new_f_name_line = generator_lines[ feature_index ];
|
|
1097
|
+
|
|
1098
|
+
} else {
|
|
1099
|
+
// If no existing tags, add new tag line
|
|
1100
|
+
new_f_name_line = `${ tag_indicator }\n${ generator_lines[ feature_index ] }`;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
generator_lines[ feature_index ] = `${ new_f_name_line } ${ name_indicator }`;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
|
|
1107
|
+
function get_constraints_Step({ generator_Scenario }) {
|
|
1108
|
+
/** Return all the constraint Steps objects in a generator Scenario. Give
|
|
1109
|
+
* warnings if needed. There should only be 1, but we all make mistakes. If
|
|
1110
|
+
* there is an error, return the ALKiln error object.
|
|
1111
|
+
*
|
|
1112
|
+
* @param {object} obj - Named arguments
|
|
1113
|
+
* @param {object} obj.generator_Scenario - A valid Gherkin AST object of one Scenario
|
|
1114
|
+
*
|
|
1115
|
+
* @returns {object} rtn - Named return values
|
|
1116
|
+
* @returns {[object]} rtn.constraints_Steps - 0 or more constraints Steps in
|
|
1117
|
+
* this Scenario.
|
|
1118
|
+
* @returns {[str]} rtn.warnings - 0 or more warning messages.
|
|
1119
|
+
* @returns {[Error]} rtn.errors - 0 or more Errors.
|
|
1120
|
+
* */
|
|
1121
|
+
let constraints_Steps = [];
|
|
1122
|
+
let errors = [];
|
|
1123
|
+
let warnings = [];
|
|
1124
|
+
|
|
1125
|
+
for ( let step of generator_Scenario.steps ) {
|
|
1126
|
+
let possible_constraints_Step = get_if_is_constraints_Step({ step });
|
|
1127
|
+
if ( possible_constraints_Step === null ) { continue; }
|
|
1128
|
+
|
|
1129
|
+
constraints_Steps.push( possible_constraints_Step );
|
|
1130
|
+
|
|
1131
|
+
if ( !possible_constraints_Step.dataTable ) {
|
|
1132
|
+
|
|
1133
|
+
let msg = `The **table** of the constrained random answers Step is missing in the Scenario at line ${ generator_Scenario.location.line } - "${ generator_Scenario.name }"`;
|
|
1134
|
+
let error = log.warn({ code: `ALK0239`, context: `generating files`, }, msg );
|
|
1135
|
+
errors.push( new Error( error ));
|
|
1136
|
+
|
|
1137
|
+
} else if ( possible_constraints_Step.dataTable.rows.length === 1 ) {
|
|
1138
|
+
|
|
1139
|
+
let msg = `There is only a header row in the **table** of the constrained random answers Step in the Scenario at line ${ generator_Scenario.location.line } - "${ generator_Scenario.name }"`;
|
|
1140
|
+
let error = log.warn({ code: `ALK0242`, context: `generating files`, }, msg );
|
|
1141
|
+
errors.push( new Error( error ));
|
|
1142
|
+
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
} // ends for every step
|
|
1146
|
+
|
|
1147
|
+
let num_Steps_found = constraints_Steps.length;
|
|
1148
|
+
|
|
1149
|
+
if ( num_Steps_found > 1 ) {
|
|
1150
|
+
let msg = `A generator Scenario can only have 1 constraints Step. The Scenario at line ${ generator_Scenario.location.line } has ${ num_Steps_found } constraints Steps. The Scenario description: "${ generator_Scenario.name }"`
|
|
1151
|
+
let error = log.warn({ code: `ALK0240`, context: `generating files`, }, msg );
|
|
1152
|
+
return { constraints_Step: null, errors: [ ...errors, new Error( error )] };
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
if ( num_Steps_found === 0 ) {
|
|
1156
|
+
// TODO: link to docs for the right syntax
|
|
1157
|
+
let msg = `The generator Scenario is missing a constrained random answers Step. Is there a typo in the Step? The Scenario starts at line ${ generator_Scenario.location.line } - "${ generator_Scenario.name }"`;
|
|
1158
|
+
let error = log.warn({ code: `ALK0241`, context: `generating files`, }, msg );
|
|
1159
|
+
return { constraints_Step: null, errors: [ ...errors, new Error( error )] };
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
return { constraints_Step: constraints_Steps[0], warnings, errors };
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
|
|
1166
|
+
// // Tests to find the step
|
|
1167
|
+
// // TODO: Consider adding "pick" and "choose" as alternatives for "constrain"
|
|
1168
|
+
// const CONSTRAIN_REGEX = /constrain/i; // constrained/constraint/constraints
|
|
1169
|
+
// const RAND_REGEX = /rand/i;
|
|
1170
|
+
// const GENERAT_REGEX = /generat/i; // generate/generates/generated/generating
|
|
1171
|
+
// const MAKE_REGEX = /mak|mad/i; // make/makes/made/making
|
|
1172
|
+
function get_if_is_constraints_Step({ step }) {
|
|
1173
|
+
/** Return null or a constraints Step.
|
|
1174
|
+
*
|
|
1175
|
+
* @param {object} obj - Named arguments
|
|
1176
|
+
* @param {object} obj.step - A valid constraints Step Gherkin object
|
|
1177
|
+
*
|
|
1178
|
+
* @returns {null | object} `null` or a constraints Step
|
|
1179
|
+
* */
|
|
1180
|
+
// Use `.test()` without `g` flag. See
|
|
1181
|
+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test#using_test_on_a_regex_with_the_global_flag
|
|
1182
|
+
let is_constraints_step = CONSTRAIN_REGEX.test( step.text )
|
|
1183
|
+
&& ( RAND_REGEX.test( step.text )
|
|
1184
|
+
|| GENERAT_REGEX.test( step.text )
|
|
1185
|
+
|| MAKE_REGEX.test( step.text )
|
|
1186
|
+
);
|
|
1187
|
+
// Example: And I generate 34 tests that get to any of ["end"] when I pick from these constraints:
|
|
1188
|
+
// Example: And I make 1 constrained random test that get to "end1", "end 2", or "end-3" with:
|
|
1189
|
+
if ( is_constraints_step ) {
|
|
1190
|
+
return step;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
return null;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
|
|
1197
|
+
function get_unique_Story_Tables_strings({ num_to_generate, ids, step }) {
|
|
1198
|
+
/** Return all the necessary randomized Story Tables as strings. Make sure
|
|
1199
|
+
* they are unique.
|
|
1200
|
+
*
|
|
1201
|
+
* @param {object} obj - Named arguments
|
|
1202
|
+
* @param {int} obj.num_to_generate Number of Story tables to generate
|
|
1203
|
+
* @param {[string]} obj.ids The question ids that any of the Story Tables can
|
|
1204
|
+
* reach
|
|
1205
|
+
* @parm {object} obj.step A valid constraints Step Gherkin object
|
|
1206
|
+
*
|
|
1207
|
+
* @returns {object} rtn - Named return values
|
|
1208
|
+
* @returns {[str]} rtn.constraints_Steps - 0 or more Story Tables (see docs).
|
|
1209
|
+
* @returns {[str]} rtn.warnings - 0 or more warning messages.
|
|
1210
|
+
* @returns {[Error]} rtn.errors - 0 or more Errors.
|
|
1211
|
+
* */
|
|
1212
|
+
let story_tables = [];
|
|
1213
|
+
let warnings = [];
|
|
1214
|
+
let errors = [];
|
|
1215
|
+
|
|
1216
|
+
// Each new Story Table Step starts the same way
|
|
1217
|
+
let step_indent = get_authors_indent({ located: step });
|
|
1218
|
+
let table_start = `${ step_indent }And I get to any of the question ids [${ ids.join(`, `) }] with this data:`;
|
|
1219
|
+
|
|
1220
|
+
for ( let table_i = 0; table_i < num_to_generate; table_i++ ) {
|
|
1221
|
+
|
|
1222
|
+
let num_attempts = 0;
|
|
1223
|
+
let max_attempts = 10;
|
|
1224
|
+
let is_unique = false;
|
|
1225
|
+
|
|
1226
|
+
while ( !is_unique && num_attempts < max_attempts ) {
|
|
1227
|
+
|
|
1228
|
+
let story_table = get_random_Story_Table({
|
|
1229
|
+
table_start,
|
|
1230
|
+
rows: step.dataTable.rows
|
|
1231
|
+
});
|
|
1232
|
+
|
|
1233
|
+
if ( story_tables.includes( story_table )) {
|
|
1234
|
+
num_attempts += 1;
|
|
1235
|
+
continue;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
is_unique = true;
|
|
1239
|
+
story_tables.push( story_table );
|
|
1240
|
+
|
|
1241
|
+
} // ends while table is not unique
|
|
1242
|
+
} // ends for number of tests
|
|
1243
|
+
|
|
1244
|
+
// // Discuss: Warn of missing table?
|
|
1245
|
+
// if ( story_tables.length === 0 ) {}
|
|
1246
|
+
|
|
1247
|
+
if ( story_tables.length < num_to_generate ) {
|
|
1248
|
+
// Discuss: Debug log or warning to developer? If the latter, need to make it pretty
|
|
1249
|
+
warnings.push(
|
|
1250
|
+
log.warn({ code: `ALK0243`, context: `generating files`, },
|
|
1251
|
+
`ALKiln only managed to make ${ story_tables.length } unique tests out of the ${ num_to_generate } asked for given the options:`,
|
|
1252
|
+
get_flatter_Step({ step })
|
|
1253
|
+
)
|
|
1254
|
+
);
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
return { story_tables, warnings, errors };
|
|
1258
|
+
};
|
|
1259
|
+
|
|
1260
|
+
|
|
1261
|
+
function get_random_Story_Table({ table_start, rows }) {
|
|
1262
|
+
/** Return a randomized Story Table string
|
|
1263
|
+
*
|
|
1264
|
+
* @param {object} obj - Named arguments
|
|
1265
|
+
* @param {string} obj.table_start - The start of the Story Table Step
|
|
1266
|
+
* @param {object} obj.rows - Gherkin AST of a `.dataTable.rows`
|
|
1267
|
+
*
|
|
1268
|
+
* @returns {string} - String of the whole Story Table Step
|
|
1269
|
+
* */
|
|
1270
|
+
let story_table_parts = [ table_start ];
|
|
1271
|
+
|
|
1272
|
+
for ( let row_i = 0; row_i < rows.length; row_i++ ) {
|
|
1273
|
+
|
|
1274
|
+
let row = rows[ row_i ];
|
|
1275
|
+
|
|
1276
|
+
if ( row_i === 0 ) {
|
|
1277
|
+
story_table_parts.push( get_table_header({ row }) );
|
|
1278
|
+
continue;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
story_table_parts.push( get_random_table_row({ row }) );
|
|
1282
|
+
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
let story_table = story_table_parts.join(`\n`);
|
|
1286
|
+
return story_table;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
|
|
1290
|
+
function get_flatter_Step({ step }) {
|
|
1291
|
+
/** Given a Gherkin `step` object, returns an object that is a less nested
|
|
1292
|
+
* version of the `step` object.
|
|
1293
|
+
*
|
|
1294
|
+
* Yes, this is annoying to test, but it's a fairly simple way to show enough
|
|
1295
|
+
* info about the Step to be useful.
|
|
1296
|
+
*
|
|
1297
|
+
* Discuss: Should this go in the Log class?
|
|
1298
|
+
*
|
|
1299
|
+
* @param {object} obj - Named arguments
|
|
1300
|
+
* @param {object} obj.step - Valid Gherkin `step` object
|
|
1301
|
+
*
|
|
1302
|
+
* @returns {{ text, rows: [[str1, str2], ...] }} - Flatter version of `step`
|
|
1303
|
+
* */
|
|
1304
|
+
let simple = { text: step.text, rows: [] };
|
|
1305
|
+
for ( let row of step.dataTable.rows ) {
|
|
1306
|
+
let cells = [];
|
|
1307
|
+
for ( let cell of row.cells ) {
|
|
1308
|
+
cells.push( cell.value );
|
|
1309
|
+
}
|
|
1310
|
+
simple.rows.push( cells );
|
|
1311
|
+
}
|
|
1312
|
+
return simple;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
|
|
1316
|
+
function get_table_header({ row }) {
|
|
1317
|
+
/** Return the string for the Story Table header row. 2 or 3 column tables are
|
|
1318
|
+
* allowed.
|
|
1319
|
+
*
|
|
1320
|
+
* Discuss: Warn about wrong number of columns?
|
|
1321
|
+
*
|
|
1322
|
+
* @param {object} obj - Named arguments
|
|
1323
|
+
* @param {object} obj.row Valid Gherkin dataTable.row object
|
|
1324
|
+
*
|
|
1325
|
+
* @returns {string} A markdown table row with 2 or 3 columns
|
|
1326
|
+
* */
|
|
1327
|
+
let indent = get_authors_indent({ located: row });
|
|
1328
|
+
let num_cols = row.cells.length;
|
|
1329
|
+
let header = `${ indent }| var | value |`;
|
|
1330
|
+
if ( num_cols >= 3 ) { header += ` trigger |`; }
|
|
1331
|
+
return header;
|
|
1332
|
+
};
|
|
1333
|
+
|
|
1334
|
+
|
|
1335
|
+
function get_random_table_row({ row }) {
|
|
1336
|
+
/** Return the string of one Story Table row with a randomly chosen answer.
|
|
1337
|
+
*
|
|
1338
|
+
* @param {object} obj - Named arguments
|
|
1339
|
+
* @param {object} obj.row - Valid Gherkin dataTable.row object
|
|
1340
|
+
*
|
|
1341
|
+
* @returns {string} A markdown table row with 2 or 3 columns
|
|
1342
|
+
* */
|
|
1343
|
+
let indent = get_authors_indent({ located: row });
|
|
1344
|
+
let cells = row.cells;
|
|
1345
|
+
let num_cols = cells.length;
|
|
1346
|
+
|
|
1347
|
+
let answer = pick_random_answer({ answers_str: cells[1].value });
|
|
1348
|
+
|
|
1349
|
+
let row_str = `${ indent }| ${ cells[0].value } | ${ answer } |`;
|
|
1350
|
+
if ( cells.length >= 3 ) { row_str += ` ${ cells[2].value } |`; }
|
|
1351
|
+
return row_str;
|
|
1352
|
+
};
|
|
1353
|
+
|
|
1354
|
+
|
|
1355
|
+
// Make a placeholder where we will make the scenario unique
|
|
1356
|
+
let ID_PLACEHOLDER = `ALKILN_SCENARIO_RANDOM_ID_PLACEHOLDER`;
|
|
1357
|
+
function get_generated_Scenarios_str({
|
|
1358
|
+
all_scenarios_top,
|
|
1359
|
+
warnings_Step = null,
|
|
1360
|
+
generator_id,
|
|
1361
|
+
story_tables,
|
|
1362
|
+
all_scenarios_bottom
|
|
1363
|
+
}) {
|
|
1364
|
+
/** Return the string for the combined Scenarios.
|
|
1365
|
+
*
|
|
1366
|
+
* @param {object} obj - Named arguments
|
|
1367
|
+
* @param {string} obj.all_scenarios_top - Scenario text that comes before the
|
|
1368
|
+
* constraints Step
|
|
1369
|
+
* @param {string} obj.warnings_Step - Optional. Default is `null`. Step to
|
|
1370
|
+
* show the author a warning message.
|
|
1371
|
+
* @param {int | string} obj.generator_id - ID of the generating Scenario
|
|
1372
|
+
* @param {[string]} obj.story_tables - Story Table text for every Scenario
|
|
1373
|
+
* that we need to generate
|
|
1374
|
+
* @param {string} obj.all_scenarios_bottom - Scenario text that comes after the
|
|
1375
|
+
* constraints Step
|
|
1376
|
+
*
|
|
1377
|
+
* @returns {[string]} - List of complete Scenario strings
|
|
1378
|
+
* */
|
|
1379
|
+
let scenarios = [];
|
|
1380
|
+
for ( let table_i = 0; table_i < story_tables.length; table_i++ ) {
|
|
1381
|
+
|
|
1382
|
+
let generator_num = generator_id + 1;
|
|
1383
|
+
let scenario_num = table_i + 1;
|
|
1384
|
+
let total_scenarios = story_tables.length;
|
|
1385
|
+
let scenario_tag = `generator${ generator_num }_scenario${ scenario_num }_of_${ total_scenarios }`;
|
|
1386
|
+
let with_tag = all_scenarios_top.replaceAll( ID_PLACEHOLDER, scenario_tag );
|
|
1387
|
+
let random_top = with_tag.replaceAll( SCENARIO_COUNT_PLACEHOLDER, `${ scenario_num } of ${ total_scenarios }` );
|
|
1388
|
+
|
|
1389
|
+
let story_table = story_tables[ table_i ];
|
|
1390
|
+
|
|
1391
|
+
let scenario_items = [ random_top ];
|
|
1392
|
+
if ( warnings_Step ) { scenario_items.push( warnings_Step ); }
|
|
1393
|
+
scenario_items.push( story_table, all_scenarios_bottom );
|
|
1394
|
+
|
|
1395
|
+
let scenario_str = scenario_items.join(`\n`);
|
|
1396
|
+
scenarios.push( scenario_str );
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
let this_Scenarios_strings = scenarios.join(`\n`);
|
|
1400
|
+
return this_Scenarios_strings;
|
|
1401
|
+
};
|
|
1402
|
+
|
|
1403
|
+
|
|
1404
|
+
let SCENARIO_COUNT_PLACEHOLDER = `ALKILN_GENEREATED_SCENARIO_COUNT_PLACEHOLDER`;
|
|
1405
|
+
function get_pre_constraints({
|
|
1406
|
+
generator_Scenario,
|
|
1407
|
+
constraints_Step,
|
|
1408
|
+
generator_lines,
|
|
1409
|
+
has_warnings
|
|
1410
|
+
}) {
|
|
1411
|
+
/** Return the Scenario text that comes before the first constraints Step.
|
|
1412
|
+
*
|
|
1413
|
+
* @param {object} obj - Named arguments
|
|
1414
|
+
* @param {object} obj.generator_Scenario - AST of the Scenario
|
|
1415
|
+
* @param {object} obj.constraints_Step - AST of the constraints Step
|
|
1416
|
+
* Step
|
|
1417
|
+
* @param {[string]} obj.generator_lines - List of lines from the generator
|
|
1418
|
+
* file
|
|
1419
|
+
* @param {boolean} obj.has_warnings - `true` if generating the Scenario
|
|
1420
|
+
* created warnings
|
|
1421
|
+
*
|
|
1422
|
+
* @returns {string} - Scenario text that comes before the first constraints
|
|
1423
|
+
* Step
|
|
1424
|
+
* */
|
|
1425
|
+
// Use a shorter name as there are a lot of long lines of code
|
|
1426
|
+
let lines = generator_lines;
|
|
1427
|
+
set_tags_and_description({ scenario: generator_Scenario, lines, has_warnings });
|
|
1428
|
+
let start_line_num = get_Scenario_start_line_num({ scenario: generator_Scenario });
|
|
1429
|
+
let top_start_index = as_index( start_line_num );
|
|
1430
|
+
|
|
1431
|
+
let top_end_index = get_index_before({ line_num: constraints_Step.location.line });
|
|
1432
|
+
|
|
1433
|
+
// Turn the list of lines for the pre-constraints section of the Scenario into
|
|
1434
|
+
// a text block
|
|
1435
|
+
let scenario_top_text = lines.slice(
|
|
1436
|
+
top_start_index,
|
|
1437
|
+
top_end_index + 1 // `slice` excludes the terminal number
|
|
1438
|
+
).join(`\n`);
|
|
1439
|
+
|
|
1440
|
+
return scenario_top_text;
|
|
1441
|
+
};
|
|
1442
|
+
|
|
1443
|
+
|
|
1444
|
+
function set_tags_and_description({ scenario, lines, has_warnings }) {
|
|
1445
|
+
/** Mutate the file lines to add placeholders for scenario text that will
|
|
1446
|
+
* show it is a generated test and give it unique identifiers.
|
|
1447
|
+
*
|
|
1448
|
+
*
|
|
1449
|
+
* @param {object} obj - Named arguments
|
|
1450
|
+
* @param {object} obj.scenario - AST of a Gherkin scenario
|
|
1451
|
+
* @param {object} obj.lines - File text lines to mutate with the new text
|
|
1452
|
+
* @param {object} obj.has_warnings - `true` if the Scenario creation caused
|
|
1453
|
+
* warning messages
|
|
1454
|
+
*
|
|
1455
|
+
* @returns {undefined}
|
|
1456
|
+
* */
|
|
1457
|
+
|
|
1458
|
+
/**
|
|
1459
|
+
* Add ALKiln tags by mutating the tag line if it exists. If not, mutate the
|
|
1460
|
+
* keyword/description line by adding ALKiln's tags above the existing
|
|
1461
|
+
* text.
|
|
1462
|
+
* */
|
|
1463
|
+
let pre_description = ``;
|
|
1464
|
+
|
|
1465
|
+
let alkiln_tags = [ `@${ ID_PLACEHOLDER }` ];
|
|
1466
|
+
if ( has_warnings ) { alkiln_tags.push(`@alkiln_warning`); }
|
|
1467
|
+
|
|
1468
|
+
if ( scenario.tags.length > 0 ) {
|
|
1469
|
+
// Mutate the tags line to add new tags
|
|
1470
|
+
let tags_index = as_index( scenario.tags[0].location.line );
|
|
1471
|
+
let new_tags = [ lines[ tags_index ], ...alkiln_tags ];
|
|
1472
|
+
lines[ tags_index ] = new_tags.join(` `);
|
|
1473
|
+
|
|
1474
|
+
} else {
|
|
1475
|
+
// Add an artificial new line with alkiln's tags
|
|
1476
|
+
pre_description = `${ alkiln_tags.join(" ")}\n`;
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
let description_index = as_index( scenario.location.line );
|
|
1480
|
+
let new_description = `${ lines[ description_index ] } (${ SCENARIO_COUNT_PLACEHOLDER })`;
|
|
1481
|
+
// Potentially mutate the new_description line
|
|
1482
|
+
lines[ description_index ] = `${ pre_description }${ new_description }`;
|
|
1483
|
+
};
|
|
1484
|
+
|
|
1485
|
+
|
|
1486
|
+
function get_post_constraints({ next_Scenario, constraints_Step, generator_lines }) {
|
|
1487
|
+
/** Return the Scenario text that comes after the last constraints Step.
|
|
1488
|
+
*
|
|
1489
|
+
* @param {object} obj - Named arguments
|
|
1490
|
+
* @param {object | undefined} obj.next_Scenario - AST of the Scenario after this Scenario
|
|
1491
|
+
* @param {object} obj.constraints_Step - AST of the constraints Step
|
|
1492
|
+
* @param {[string]} obj.generator_lines - List of lines from the generator file
|
|
1493
|
+
*
|
|
1494
|
+
* @returns {string} - Scenario text that comes after the last constraints
|
|
1495
|
+
* Step
|
|
1496
|
+
* */
|
|
1497
|
+
let rows = constraints_Step.dataTable.rows;
|
|
1498
|
+
let constraints_end_index = as_index( rows[ rows.length - 1].location.line );
|
|
1499
|
+
let bottom_start_index = constraints_end_index + 1;
|
|
1500
|
+
|
|
1501
|
+
if ( !next_Scenario ) {
|
|
1502
|
+
// Get lines till the end of the file
|
|
1503
|
+
let scenario_bottom_text = generator_lines.slice(
|
|
1504
|
+
bottom_start_index
|
|
1505
|
+
).join(`\n`);
|
|
1506
|
+
return scenario_bottom_text;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
// Get lines till the start of the next Scenario
|
|
1510
|
+
let next_Scenario_start_line = get_Scenario_start_line_num({ scenario: next_Scenario });
|
|
1511
|
+
let bottom_end_index = get_index_before({ line_num: next_Scenario_start_line });
|
|
1512
|
+
|
|
1513
|
+
let scenario_bottom_text = generator_lines.slice(
|
|
1514
|
+
bottom_start_index,
|
|
1515
|
+
bottom_end_index + 1 // `slice` excludes the terminal number
|
|
1516
|
+
).join(`\n`);
|
|
1517
|
+
|
|
1518
|
+
return scenario_bottom_text;
|
|
1519
|
+
};
|
|
1520
|
+
|
|
1521
|
+
|
|
1522
|
+
function get_index_before({ line_num }) {
|
|
1523
|
+
/** Return the index in the file line list that corresponds to the given
|
|
1524
|
+
* Gherkin 1-indexed line number. This is mostly here as a name that
|
|
1525
|
+
* clarifies what the code is doing and hides a little bit of the
|
|
1526
|
+
* complexity.
|
|
1527
|
+
*
|
|
1528
|
+
* Discuss: is get_index_before() worth abstracting?
|
|
1529
|
+
*
|
|
1530
|
+
* Each object in the Gherkin AST will have a `location` object that includes
|
|
1531
|
+
* the item's line number (`{line: 1}`). Each line number represents a line
|
|
1532
|
+
* in the file and therefore an item in the `generator_lines` list. The line
|
|
1533
|
+
* numbers are 1-indexed. That means the location of `{line: 1}` in the AST
|
|
1534
|
+
* refers to `generator_lines[0]`.
|
|
1535
|
+
*
|
|
1536
|
+
* @param {object} obj - Named arguments
|
|
1537
|
+
* @param {int} obj.line_num - Integer of a Gherkin 1-indexed line number
|
|
1538
|
+
*
|
|
1539
|
+
* @returns {int} - File-line-list index that corresponds to the Gherkin line
|
|
1540
|
+
* number
|
|
1541
|
+
* */
|
|
1542
|
+
let line_as_index = as_index( line_num );
|
|
1543
|
+
return line_as_index - 1;
|
|
1544
|
+
};
|
|
1545
|
+
|
|
1546
|
+
|
|
1547
|
+
// function get_index_after({ line_num }) {};
|
|
1548
|
+
|
|
1549
|
+
|
|
1550
|
+
function get_authors_indent({ located }) {
|
|
1551
|
+
/** Return a whitespace string representing the indentation for a given
|
|
1552
|
+
* Gherkin location object.
|
|
1553
|
+
*
|
|
1554
|
+
* @param {object} obj - Named arguments
|
|
1555
|
+
* @param {object} obj.located - Gherkin AST object with a location property
|
|
1556
|
+
*
|
|
1557
|
+
* @returns {string} - A whitespace string representing the indentation for a
|
|
1558
|
+
* given Gherkin location object.
|
|
1559
|
+
* */
|
|
1560
|
+
if ( typeof located !== 'object' || !located.location ) {
|
|
1561
|
+
return ``;
|
|
1562
|
+
}
|
|
1563
|
+
return ` `.repeat( located.location.column - 1 );
|
|
1564
|
+
};
|
|
1565
|
+
|
|
1566
|
+
|
|
1567
|
+
// const CAPTURE_NUMBERS_REGEX = /(\d+)/g;
|
|
1568
|
+
function get_number_of_Scenarios_to_generate({ step_text }) {
|
|
1569
|
+
/** Return the number of random constraints tests to generate with a minimum
|
|
1570
|
+
* of 1 test.
|
|
1571
|
+
*
|
|
1572
|
+
* Gives warnings instead of errors, defaulting to returning `1`.
|
|
1573
|
+
*
|
|
1574
|
+
* @param {object} obj - Named arguments
|
|
1575
|
+
* @param {string} obj.step_text - The text of a constraints Step
|
|
1576
|
+
*
|
|
1577
|
+
* @returns {object} rtn - Named return values
|
|
1578
|
+
* @returns {int} rtn.num_to_generate - Number of tests to generate. Min is 1.
|
|
1579
|
+
* @returns {[str]} rtn.warnings - 0 or more warning messages.
|
|
1580
|
+
* @returns {[Error]} rtn.errors - 0 or more Errors.
|
|
1581
|
+
* */
|
|
1582
|
+
let user_num = null;
|
|
1583
|
+
let warnings = [];
|
|
1584
|
+
let errors = [];
|
|
1585
|
+
let number_matcher = step_text.match( CAPTURE_NUMBERS_REGEX );
|
|
1586
|
+
|
|
1587
|
+
/** No numbers */
|
|
1588
|
+
if ( number_matcher === null ) {
|
|
1589
|
+
warnings.push(
|
|
1590
|
+
log.warn({ code: `ALK0237`, context: `generating files`, },
|
|
1591
|
+
`A constrained random answers Step is missing the number of tests to generate. ALKiln will create 1 test. The Step:\n${ step_text }`
|
|
1592
|
+
)
|
|
1593
|
+
);
|
|
1594
|
+
user_num = 1;
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
/** Too many numbers */
|
|
1598
|
+
if ( number_matcher && number_matcher.length > 1 ) {
|
|
1599
|
+
// Some feedback indicated using the last number makes more sense to people
|
|
1600
|
+
// than the first
|
|
1601
|
+
user_num = parseInt( number_matcher[ number_matcher.length - 1 ]);
|
|
1602
|
+
warnings.push(
|
|
1603
|
+
log.warn({ code: `ALK0238`, context: `generating files`, },
|
|
1604
|
+
`This constrained random answers Step is ambiguous because it has more than 1 number. ALKiln will use the last number the author gave ("${ user_num }"). The Step:\n${ step_text }`,
|
|
1605
|
+
)
|
|
1606
|
+
);
|
|
1607
|
+
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
/** Happy path */
|
|
1611
|
+
if ( number_matcher && !user_num ) {
|
|
1612
|
+
user_num = parseInt( number_matcher[ 0 ]);
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
let num_to_generate = Math.min(
|
|
1616
|
+
session_vars.get_max_tests_per_generator_scenario(),
|
|
1617
|
+
user_num
|
|
1618
|
+
);
|
|
1619
|
+
|
|
1620
|
+
if ( num_to_generate !== user_num ) {
|
|
1621
|
+
warnings.push(
|
|
1622
|
+
log.warn({ code: `ALK0253`, context: `generating files`, },
|
|
1623
|
+
`The author asked to generate ${ user_num } tests, but maximum allowed is ${ num_to_generate }. If you want to increase the maximum, use the MAX_TESTS_PER_GENERATOR_SCENARIO environment variable. Read more about environment variables at https://assemblyline.suffolklitlab.org/docs/alkiln/writing/#env-vars.`
|
|
1624
|
+
)
|
|
1625
|
+
);
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
return { num_to_generate, warnings, errors };
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
|
|
1632
|
+
function get_warnings_Step({ warnings = [], step_text }) {
|
|
1633
|
+
/** Returns the text for any warnings that need to be added to the Scenario
|
|
1634
|
+
*
|
|
1635
|
+
* @param {object} obj - Named arguments
|
|
1636
|
+
* @param {[string]} obj.warnings - 0 or more warning messages
|
|
1637
|
+
* @param {string} obj.step_text - Constraints Step text (to match indent)
|
|
1638
|
+
*
|
|
1639
|
+
* @returns {object} rtn - Named return values
|
|
1640
|
+
* @returns {str} rtn.warnings_Step - cucumberjs Step to show author a warning
|
|
1641
|
+
* @returns {[Error]} rtn.errors - 0 or more Errors.
|
|
1642
|
+
* */
|
|
1643
|
+
let errors = [];
|
|
1644
|
+
|
|
1645
|
+
if ( warnings.length === 0 ) { return { warnings_Step: null, errors }; }
|
|
1646
|
+
|
|
1647
|
+
let indent_match = step_text.match(/$\s*/);
|
|
1648
|
+
let indent = ( indent_match && indent_match[ 0 ]) || ` `;
|
|
1649
|
+
|
|
1650
|
+
let new_warnings_lines = [];
|
|
1651
|
+
new_warnings_lines.push(`${ indent }Then ALKiln warns the author about a generator problem with:`);
|
|
1652
|
+
new_warnings_lines.push(`${ indent } """`, `${ indent } ----- Generator file warning -----`);
|
|
1653
|
+
|
|
1654
|
+
for ( let warning of warnings ) {
|
|
1655
|
+
let warning_lines = warning.split(`\n`);
|
|
1656
|
+
for ( let warning_line of warning_lines ) {
|
|
1657
|
+
new_warnings_lines.push(`${ indent } ${ warning_line }`);
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
new_warnings_lines.push(`${ indent } -----`, `${ indent } """`);
|
|
1662
|
+
|
|
1663
|
+
let warnings_Step = new_warnings_lines.join(`\n`);
|
|
1664
|
+
|
|
1665
|
+
return { warnings_Step, errors };
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
|
|
1669
|
+
let CAPTURE_QUESTION_IDS_REGEX = /("[^"]+")/g;
|
|
1670
|
+
function get_final_ids({ step_text }) {
|
|
1671
|
+
/** Return a list of docassemble question block ids (in double quotes) that
|
|
1672
|
+
* are in an ALKiln constraints Step. Every generated Story Table Step must
|
|
1673
|
+
* reach one of these ids.
|
|
1674
|
+
*
|
|
1675
|
+
* @param {object} obj - Named arguments
|
|
1676
|
+
* @param {string} obj.step_text - The text of a constraints Step
|
|
1677
|
+
*
|
|
1678
|
+
* Example:
|
|
1679
|
+
* get_final_ids({ step_text: `And I make 1 constrained random test that gets to "end1" or "end 2" with:` });
|
|
1680
|
+
* // [`"end1"`, `"end2"`]
|
|
1681
|
+
*
|
|
1682
|
+
* @returns {object} rtn - Named return values
|
|
1683
|
+
* @returns {["str"]} rtn.ids - 0 or more double-quoted docassemble question
|
|
1684
|
+
* block ids. Each generated Story Table Step must end at one of these
|
|
1685
|
+
* ids. this Scenario.
|
|
1686
|
+
* @returns {[str]} rtn.warnings - 0 or more warning messages.
|
|
1687
|
+
* @returns {[Error]} rtn.errors - 0 or more Errors.
|
|
1688
|
+
* */
|
|
1689
|
+
let errors = [];
|
|
1690
|
+
let warnings = [];
|
|
1691
|
+
|
|
1692
|
+
let ids = step_text.match( CAPTURE_QUESTION_IDS_REGEX );
|
|
1693
|
+
|
|
1694
|
+
if ( ids === null ) {
|
|
1695
|
+
let missing_ids_msg = `The constrained random answers Step is missing its question ids. ALKiln is unable to create random tests for this generator Scenario. The Step:\n${ step_text }`;
|
|
1696
|
+
let error = log.warn({ code: `ALK0245`, context: `generating files`, },
|
|
1697
|
+
missing_ids_msg
|
|
1698
|
+
);
|
|
1699
|
+
errors.push( new Error( error ));
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
return { ids, warnings, errors };
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
|
|
1706
|
+
function pick_random_answer({ answers_str=`` }) {
|
|
1707
|
+
/** Return one of the answers from the given string of possible answers.
|
|
1708
|
+
*
|
|
1709
|
+
* @param {object} obj - Named arguments
|
|
1710
|
+
* @param {str} obj.answers_str - A string representing one or more possible
|
|
1711
|
+
* answers. The possible answers are separated by this character
|
|
1712
|
+
* combination: `;;`
|
|
1713
|
+
*
|
|
1714
|
+
* Example:
|
|
1715
|
+
* pick_random_answer({ answers_str: `yes;; no;; maybe` });
|
|
1716
|
+
* // `no`
|
|
1717
|
+
*
|
|
1718
|
+
* TODO: Add a warning in the docs that Gherkin ignores whitespace at the
|
|
1719
|
+
* start and end of columns in a table. Discuss letting authors wrap
|
|
1720
|
+
* content in backticks or some other delimiter.
|
|
1721
|
+
*
|
|
1722
|
+
* @returns {string} - One of the possible answers
|
|
1723
|
+
* */
|
|
1724
|
+
let choices = answers_str.replace(`;; `, `;;`).split(`;;`);
|
|
1725
|
+
if ( choices.includes(``) ) {
|
|
1726
|
+
log.debug({ code: `ALK0244`, context: `generating files`, level: `warn` },
|
|
1727
|
+
`**Quiet warning** - One of the constrained random answer choices in a row in this table is "". It could be on purpose or it could be a misplaced separator (";;"). The choices: "${ answers_str }"`
|
|
1728
|
+
);
|
|
1729
|
+
}
|
|
1730
|
+
let choice_index = Math.floor( Math.random() * choices.length );
|
|
1731
|
+
return choices[ choice_index ];
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
|
|
1735
|
+
function get_row_as_string({ row }) {
|
|
1736
|
+
/** Return the given Gherkin row as a single markdown table row */
|
|
1737
|
+
let cells = row.cells;
|
|
1738
|
+
let values = cells.map(( cell ) => { return cell.value; });
|
|
1739
|
+
let row_str = `| ${ values.join( ` | ` ) } |`;
|
|
1740
|
+
return row_str;
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
|
|
1744
|
+
function get_Scenario_start_line_num({ scenario }) {
|
|
1745
|
+
/** Returns the Gherkin line number where the Scenario really starts. That
|
|
1746
|
+
* could be where the tags are or where the Scenario keyword is.
|
|
1747
|
+
*
|
|
1748
|
+
* @param {object} obj - Named arguments
|
|
1749
|
+
* @param {object} obj.scenario - The Gherkin AST for a Scenario
|
|
1750
|
+
*
|
|
1751
|
+
* @returns {int} - Gherkin line number where the Scenario starts
|
|
1752
|
+
* */
|
|
1753
|
+
if ( scenario.tags.length > 0 ) {
|
|
1754
|
+
return scenario.tags[0].location.line;
|
|
1755
|
+
} else {
|
|
1756
|
+
return scenario.location.line;
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
|
|
1761
|
+
function as_index( line_num ) {
|
|
1762
|
+
/** Given a Gherkin line number, return the file-lines-list index for that
|
|
1763
|
+
* line number. This is here mostly as a name that helps clarify what the
|
|
1764
|
+
* code is doing.
|
|
1765
|
+
*
|
|
1766
|
+
* Each Gherkin line number matches a line in the file and therefore an item
|
|
1767
|
+
* in the `generator_lines` list. The Gherkin line numbers are 1-indexed. That
|
|
1768
|
+
* means the location of `{line: 1}` in the AST refers to `generator_lines[0]`.
|
|
1769
|
+
*
|
|
1770
|
+
* @param {int} line_num - A 1-indexed Gherkin line number
|
|
1771
|
+
*
|
|
1772
|
+
* @returns {int} - The corresponding index in the list of lines in the
|
|
1773
|
+
* generator file
|
|
1774
|
+
* */
|
|
1775
|
+
return line_num - 1;
|
|
1776
|
+
}
|