@suffolklitlab/alkiln 5.13.2 → 5.13.3-feat-random-2
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 +10 -8
- package/.github/workflows/playground.yml +5 -2
- package/.github/workflows/unit_tests.yml +1 -1
- package/CHANGELOG.md +32 -1
- package/_alkiln_meta/shift_logs.js +75 -0
- package/action.yml +89 -31
- package/action_for_github_server/action.yml +65 -29
- package/docs/4_environments.md +243 -0
- package/docs/artifacts_folder_name.md +135 -0
- package/docs/constrained_random_tests.md +254 -0
- package/docs/decisions/006_which_log_level_to_pick_2024_09_5.md +70 -0
- package/docs/decisions/007_getting_and_storing_logs.md +179 -0
- package/docs/decisions/move_runtime_config_to_sources_folder.md +5 -0
- package/docs/decisions/reports_and_logs_2024_08_11.md +161 -0
- package/docs/kinds_of_errors.md +26 -0
- package/docs/prioritize_constrained_random_tests.md +60 -0
- package/lib/docassemble/docassemble_api_interface.js +77 -51
- package/lib/docassemble/server_install.js +32 -9
- package/lib/docassemble/setup.js +40 -25
- package/lib/docassemble/takedown.js +33 -10
- package/lib/globals.js +10 -0
- package/lib/run_cucumber.js +141 -69
- package/lib/scope.js +101 -77
- package/lib/setup/TestGenerator.js +1755 -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 +132 -54
- package/lib/utils/Log.js +1145 -0
- package/lib/utils/_alkiln-constrained_random_tests_old.js +1776 -0
- package/lib/utils/files.js +18 -11
- package/lib/utils/get_error_stacks.js +16 -0
- package/lib/utils/reports.js +100 -34
- package/lib/utils/session_vars.js +107 -73
- package/lib/utils/set_sources_paths.js +98 -0
- package/lib/utils/time.js +44 -0
- package/package.json +18 -7
- package/tests/log_codes/check_codes.sh +18 -13
- package/tests/log_codes/log_code_expected_instances.txt +91 -9
- package/tests/unit_tests/constrained_random_answers/invalid.fixtures.js +177 -0
- package/tests/unit_tests/constrained_random_answers/invalid.test.js +251 -0
- package/tests/unit_tests/constrained_random_answers/valid.fixtures.js +936 -0
- package/tests/unit_tests/constrained_random_answers/valid.test.js +541 -0
- package/tests/unit_tests/example_of_chai_test_lifecycle.js +78 -0
- package/tests/unit_tests/getAllFields.test.js +5 -0
- package/tests/unit_tests/getMatchingRows.test.js +5 -0
- package/tests/unit_tests/log.test.js +557 -0
- package/tests/unit_tests/normalizeTable.test.js +5 -0
- package/lib/utils/log.js +0 -96
- /package/docs/{expected_inernal_results.md → expected_internal_results.md} +0 -0
|
@@ -0,0 +1,1755 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const fg = require(`fast-glob`);
|
|
3
|
+
|
|
4
|
+
const globals = require(`../globals`);
|
|
5
|
+
const session_vars = require('../utils/session_vars');
|
|
6
|
+
const get_Gherkin_AST = require(`./get_Gherkin_AST`);
|
|
7
|
+
const get_error_stacks = require(`../utils/get_error_stacks`);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* TODO: We might give various levels of categories for warnings. For example,
|
|
11
|
+
* severe warnings (like "will skip the random test") vs. slight warnings
|
|
12
|
+
* (like "missing number in constraints random test")
|
|
13
|
+
* TODO: Post-MVP, don't repeat tests that the author has already created
|
|
14
|
+
* TODO: Post-MVP, write Steps that warn about fore-Steps and constraints Steps
|
|
15
|
+
* when they appear in a regular test. It probably means someone named the
|
|
16
|
+
* file incorrectly.
|
|
17
|
+
* Discuss: Require author to use "Generator Scenario:" as they keyword (Also
|
|
18
|
+
* "Generator Scenario Outline:") (or "Scenario Generator", maybe harder to
|
|
19
|
+
* scan) for 2 reasons. 1. Gets the vocab of "generator" in early (though
|
|
20
|
+
* that doesn't match "constraints" Step, which is a whole other language
|
|
21
|
+
* problem we haven't solved yet.) 2. Saying "MAX_RANDOM_TESTS_PER_SCENARIO"
|
|
22
|
+
* is confusing when "Scenario" means different things - a generator
|
|
23
|
+
* Scenario and a regular Scenario.
|
|
24
|
+
* Discuss: Now that Log is less fragile and duplicated, shall we create a new
|
|
25
|
+
* Log instance in here?
|
|
26
|
+
* Discuss: consider debug types that are for authors. Sort of like warnings,
|
|
27
|
+
* but less loud.
|
|
28
|
+
* Discuss: Log.error that just returns a string formatted like an error with
|
|
29
|
+
* the right icon, etc, without logging it to the console. We can _sort_ of
|
|
30
|
+
* do that now, but without the icon.
|
|
31
|
+
* Discuss: If/when we allow multiple Scenarios, research how we find the end
|
|
32
|
+
* of a Scenario that ends in an `Example`.
|
|
33
|
+
* */
|
|
34
|
+
|
|
35
|
+
let log = null;
|
|
36
|
+
const FOLDER_NAME = globals.generated_files_folder_name;
|
|
37
|
+
|
|
38
|
+
module.exports = class TestGenerator {
|
|
39
|
+
constructor() {
|
|
40
|
+
this.all_Scenarios_bodies_map = {}; // map for O^1 adding operation
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
create({ sources_path, logger }) {
|
|
44
|
+
/** Given files with a certain prefix, use the text in them to generate a
|
|
45
|
+
* feature file or files. A file generates one file with multiple
|
|
46
|
+
* Scenarios.
|
|
47
|
+
*
|
|
48
|
+
* @param {object} obj - Named arguments
|
|
49
|
+
* @param {string} obj.sources_path - Path to the docassemble Sources folder
|
|
50
|
+
*
|
|
51
|
+
* @returns {object} rtn - Named return values
|
|
52
|
+
* @returns {[str]} rtn.new_strings - 0 or more strings that each created a
|
|
53
|
+
* new file.
|
|
54
|
+
* @returns {[Error|str]} rtn.errors - 0 or more strings or Errors
|
|
55
|
+
*
|
|
56
|
+
* Details:
|
|
57
|
+
* - Copy sections of text from the old file
|
|
58
|
+
* - Use the Gherkin parser's AST of the constraints Step in each Scenario
|
|
59
|
+
* to generate Story Tables
|
|
60
|
+
* - Combine various sections with the Story Tables
|
|
61
|
+
* - Make a new file with that content
|
|
62
|
+
*
|
|
63
|
+
* We're trying to find the least complex, yet sufficiently robust, system
|
|
64
|
+
*
|
|
65
|
+
* Goal:
|
|
66
|
+
* Stay as true to the author's syntax as possible so they can get useful
|
|
67
|
+
* errors from cucumber itself.
|
|
68
|
+
*
|
|
69
|
+
* Anti-goals:
|
|
70
|
+
* - Be cucumber
|
|
71
|
+
* - Render the whole AST ourselves
|
|
72
|
+
*
|
|
73
|
+
* For examples, see tests.
|
|
74
|
+
*
|
|
75
|
+
* */
|
|
76
|
+
log = logger;
|
|
77
|
+
|
|
78
|
+
let new_files_strings = [];
|
|
79
|
+
let all_errors = [];
|
|
80
|
+
|
|
81
|
+
// Get the paths of the source files that we need to parse
|
|
82
|
+
let generator_paths = fg.sync(`${ sources_path }/${ globals.generators_prefix }*.feature`);
|
|
83
|
+
for ( let generator_path of generator_paths ) {
|
|
84
|
+
|
|
85
|
+
let file_text = fs.readFileSync( generator_path, {encoding: `utf8`});
|
|
86
|
+
|
|
87
|
+
log.info({ code: `ALK0231`, context: `generating files`, },
|
|
88
|
+
`ALKiln is trying to generate the constrained random answers tests for "${ generator_path }"`
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
let { new_contents, errors: parse_errors } = this.parse_file({
|
|
92
|
+
file_text,
|
|
93
|
+
generator_path
|
|
94
|
+
});
|
|
95
|
+
all_errors.push( ...parse_errors );
|
|
96
|
+
|
|
97
|
+
if ( new_contents ) {
|
|
98
|
+
new_files_strings.push( new_contents );
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
log.debug({ code: `ALK0267`, context: `generating files`, }, `Constrained random answers tests:`, new_files_strings );
|
|
103
|
+
|
|
104
|
+
let tests_path = `${ sources_path }/${ FOLDER_NAME }`;
|
|
105
|
+
let artifacts_path = `${ session_vars.get_artifacts_path_name() }/${ FOLDER_NAME }`;
|
|
106
|
+
save_files({ tests_path, artifacts_path, file_strings: new_files_strings });
|
|
107
|
+
|
|
108
|
+
log.info({ code: `ALK0271`, context: `generating files`, }, `Tried to save generated files to ${ artifacts_path }` );
|
|
109
|
+
|
|
110
|
+
return { new_strings: new_files_strings, errors: all_errors };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
parse_file({ file_text, generator_path, logger }) {
|
|
114
|
+
/** Parse and return one file's contents
|
|
115
|
+
*
|
|
116
|
+
* Discuss: If there's a Gherkin parser error, log something more clear
|
|
117
|
+
* before re-throwing it?
|
|
118
|
+
*
|
|
119
|
+
* Note: The generator file should be valid by the time it gets here.
|
|
120
|
+
*
|
|
121
|
+
* @param {string} file_text - Full text of a generator feature file
|
|
122
|
+
*
|
|
123
|
+
* @returns {object} rtn - Named return values
|
|
124
|
+
* @returns {null|str} rtn.new_contents - `null` or string for the new file
|
|
125
|
+
* @returns {[Error|str]} rtn.errors - 0 or more strings or Errors
|
|
126
|
+
* */
|
|
127
|
+
// Unit tests call the parser alone, so they need to create their own logger.
|
|
128
|
+
if ( logger ) {
|
|
129
|
+
log = logger;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let errors = [];
|
|
133
|
+
|
|
134
|
+
let { doc_AST, errors: generator_Gherkin_errors } = get_generator_AST({ file_text, generator_path });
|
|
135
|
+
if ( generator_Gherkin_errors.length > 0 ) {
|
|
136
|
+
return { new_contents: null, errors: generator_Gherkin_errors };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let { new_contents, errors: generating_errors } = this.get_new_file_contents({ doc_AST, file_text, generator_path });
|
|
140
|
+
if ( generating_errors.length > 0 ) {
|
|
141
|
+
return { new_contents: null, errors: generating_errors };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let { errors: generated_parsing_errors } = validate_syntax({ file_text: new_contents, generator_path });
|
|
145
|
+
if ( generated_parsing_errors.length > 0 ) {
|
|
146
|
+
let syntax_error = log.error({ code: `ALK0266`, context: `generating files`, before: `\n\n`, },
|
|
147
|
+
`The .feature file ALKiln generated from "${ generator_path }" had Gherkin syntax error(s) ALKiln did not anticipate. If you have a moment, we would love you to send us your debug.txt.\n`,
|
|
148
|
+
...get_error_stacks({ errors: generated_parsing_errors })
|
|
149
|
+
);
|
|
150
|
+
return { new_contents, errors: [new Error( syntax_error )] };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return { new_contents, errors };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
get_new_file_contents({ doc_AST={}, file_text=``, generator_path }) {
|
|
158
|
+
/** Return the text for one new file with tests based on one generator file.
|
|
159
|
+
*
|
|
160
|
+
* Note: Right now it is out of scope to save comments in constraints Steps.
|
|
161
|
+
*
|
|
162
|
+
* @param {object} obj - Named arguments
|
|
163
|
+
* @param {object} obj.doc_AST - Gherkin-parsed abstract syntax tree of
|
|
164
|
+
* generator file
|
|
165
|
+
* @param {string} obj.file_text - String of the whole generator file
|
|
166
|
+
* @param {string} obj.generator_path - Path to the generator file
|
|
167
|
+
*
|
|
168
|
+
* @returns {null|string} - The text (Feature, Scenarios, etc) for the new
|
|
169
|
+
* file
|
|
170
|
+
*
|
|
171
|
+
* @returns {object} rtn - Named return values
|
|
172
|
+
* @returns {null|str} rtn.new_contents - `null` or string for new file
|
|
173
|
+
* @returns {[Error|str]} rtn.errors - 0 or more strings or Errors
|
|
174
|
+
* */
|
|
175
|
+
|
|
176
|
+
// `doc_AST.feature.children` seem to always be defined by here
|
|
177
|
+
let scenario_objs = doc_AST.feature.children;
|
|
178
|
+
if ( !scenario_objs[0] ) {
|
|
179
|
+
let syntax_error = log.error({ code: `ALK0235`, context: `generating files`, before: `\n\n`, },
|
|
180
|
+
`The author's generator .feature file at "${ generator_path }" may be missing its Scenarios.`
|
|
181
|
+
);
|
|
182
|
+
return { new_contents: null, errors: [new Error( syntax_error )]};
|
|
183
|
+
}
|
|
184
|
+
// `scenario_objs[0].scenario` seems to always be defined by here
|
|
185
|
+
|
|
186
|
+
if ( scenario_objs.length > 1 ) {
|
|
187
|
+
let too_many_scenarios = log.error({ code: `ALK0236`, context: `generating files`, },
|
|
188
|
+
`A generator file can only have 1 generator Scenario. This one has ${ scenario_objs.length }.`
|
|
189
|
+
);
|
|
190
|
+
return { new_contents: ``, errors: [new Error( too_many_scenarios )] };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
let generation_errors = [];
|
|
194
|
+
|
|
195
|
+
let generator_lines = file_text.split(`\n`);
|
|
196
|
+
|
|
197
|
+
let feature_text = get_Feature_text({ feature: doc_AST.feature, generator_lines });
|
|
198
|
+
|
|
199
|
+
let { scenarios_strings, errors: scenarios_errors } = this.get_new_Scenarios_strs({
|
|
200
|
+
scenario_objs,
|
|
201
|
+
generator_lines
|
|
202
|
+
});
|
|
203
|
+
generation_errors.push( ...scenarios_errors );
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
let count_msg = `ALKiln generated ${ scenarios_strings.length } Scenario or Scenarios`;
|
|
207
|
+
|
|
208
|
+
if ( scenarios_strings.length === 0 ) {
|
|
209
|
+
let error = log.error({ code: `ALK0263`, context: `generating files`, }, count_msg );
|
|
210
|
+
generation_errors.push( new Error( error ));
|
|
211
|
+
return { new_contents: null, errors: generation_errors };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
log.info({ code: `ALK0264`, context: `generating files`, }, count_msg );
|
|
215
|
+
|
|
216
|
+
let scenarios_combined = scenarios_strings.join(`\n\n`);
|
|
217
|
+
let new_file_contents = [ feature_text, scenarios_combined ].join(`\n`);
|
|
218
|
+
|
|
219
|
+
if ( !new_file_contents ) {
|
|
220
|
+
// Fallback. Haven't been able to trigger this
|
|
221
|
+
let msg = `Unable to generate a constrained random answers file from "${ generator_path }". See above for warnings and errors.`;
|
|
222
|
+
let error = log.error({ code: `ALK0265`, context: `generating files`, }, msg );
|
|
223
|
+
return { new_contents: null, errors: [ new Error( error )] };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return { new_contents: new_file_contents, errors: generation_errors };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
get_new_Scenarios_strs({ scenario_objs, generator_lines }) {
|
|
231
|
+
/** Return all the strings of the generated Scenarios and any Errors we ran
|
|
232
|
+
* into.
|
|
233
|
+
*
|
|
234
|
+
* @param {object} obj - Named arguments
|
|
235
|
+
* @param {object} obj.scenario_objs - 0 or more Gherkin Scenario objects
|
|
236
|
+
* (which are generators)
|
|
237
|
+
* @param {[str]} obj.generator_lines - List of lines in the generator file
|
|
238
|
+
*
|
|
239
|
+
* Starting here, functions' return values should now include lists of
|
|
240
|
+
* warning messages. If a Scenario gets any warnings, we will add a
|
|
241
|
+
* "warnings" Step to that Scenario or to the file. Implement over time.
|
|
242
|
+
*
|
|
243
|
+
* We only allow one Scenario per file now, but in future we can allow more.
|
|
244
|
+
*
|
|
245
|
+
* @returns {object} rtn - Named return values
|
|
246
|
+
* @returns {[str]} rtn.scenarios - 0 or more Scenario strings
|
|
247
|
+
* @returns {[str]} rtn.warnings - 0 or more warning messages
|
|
248
|
+
* @returns {[Error|str]} rtn.errors - 0 or more strings or Errors
|
|
249
|
+
* */
|
|
250
|
+
let errs_n_warns = new Misbehaviors();
|
|
251
|
+
|
|
252
|
+
let strs_for_all_new_Scenarios = [];
|
|
253
|
+
// Soon we want to be able to handle multiple generator Scenarios per file
|
|
254
|
+
for ( let scenario_obj_i = 0; scenario_obj_i < scenario_objs.length; scenario_obj_i++ ) {
|
|
255
|
+
|
|
256
|
+
errs_n_warns.reset_warnings();
|
|
257
|
+
/** Start returning warnings here. */
|
|
258
|
+
let generator_Scenario = scenario_objs[ scenario_obj_i ].scenario;
|
|
259
|
+
|
|
260
|
+
let description = scenario_description_for_logs({ scenario: generator_Scenario });
|
|
261
|
+
log.info({ code: `ALK0237`, context: `generating files`, },
|
|
262
|
+
`Generating files for Scenario at ${ description }`
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
let {
|
|
266
|
+
num_to_make,
|
|
267
|
+
file_lines_to_delete
|
|
268
|
+
} = errs_n_warns.add(
|
|
269
|
+
get_fore_Steps({ scenario: generator_Scenario })
|
|
270
|
+
);
|
|
271
|
+
if ( errs_n_warns.errors.length > 0 ) { continue; }
|
|
272
|
+
|
|
273
|
+
let { new_Scenarios_text } = errs_n_warns.add(
|
|
274
|
+
this.get_1_text_block_of_all_new_Scenarios_from_1_generator_Scenario({
|
|
275
|
+
scenario: generator_Scenario,
|
|
276
|
+
generator_lines: generator_lines,
|
|
277
|
+
file_lines_to_delete,
|
|
278
|
+
num_to_make
|
|
279
|
+
})
|
|
280
|
+
);
|
|
281
|
+
if ( errs_n_warns.errors.length > 0 ) { continue; }
|
|
282
|
+
|
|
283
|
+
if ( errs_n_warns.warnings.length > 0 ) {
|
|
284
|
+
new_Scenarios_text = add_warnings_Scenario({
|
|
285
|
+
scenarios_str: new_Scenarios_text, warnings: errs_n_warns.warnings
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
log.debug({ code: `ALK0262`, context: `generating files`, }, `new_Scenarios_text with ${ errs_n_warns.warnings.length } warnings:\n`, new_Scenarios_text );
|
|
289
|
+
|
|
290
|
+
strs_for_all_new_Scenarios.push( new_Scenarios_text );
|
|
291
|
+
} // ends for every scenario_container
|
|
292
|
+
|
|
293
|
+
return { scenarios_strings: strs_for_all_new_Scenarios, ...errs_n_warns };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
get_1_text_block_of_all_new_Scenarios_from_1_generator_Scenario({
|
|
298
|
+
scenario,
|
|
299
|
+
generator_lines,
|
|
300
|
+
file_lines_to_delete,
|
|
301
|
+
num_to_make,
|
|
302
|
+
}) {
|
|
303
|
+
/** Return a list of 0 or more strings of tests generated from 1 generator
|
|
304
|
+
* Scenario Gherkin object.
|
|
305
|
+
*
|
|
306
|
+
* @param {object} obj - Named arguments
|
|
307
|
+
* @param {object} obj.scenario - Gherkin Scenario object
|
|
308
|
+
* @param {[str]} obj.generator_lines - Lines in the generator file
|
|
309
|
+
* @param {object} obj.file_lines_to_delete - Lines to avoid adding to the
|
|
310
|
+
* new file
|
|
311
|
+
* @param {object} obj.num_to_make - Number of tests to make
|
|
312
|
+
*
|
|
313
|
+
* @returns {object} rtn - Named return values
|
|
314
|
+
* @returns {string} rtn.new_Scenarios_text - Single string containing all
|
|
315
|
+
* the newly generated Scenarios
|
|
316
|
+
* @returns {[str]} rtn.warnings - 0 or more warning messages.
|
|
317
|
+
* @returns {[Error|str]} rtn.errors - 0 or more strings or Errors
|
|
318
|
+
* */
|
|
319
|
+
let misfits = new Misbehaviors();
|
|
320
|
+
|
|
321
|
+
let indexes_to_delete = file_lines_to_delete;
|
|
322
|
+
|
|
323
|
+
let new_test_bodies = [];
|
|
324
|
+
let actual_test_num = 1;
|
|
325
|
+
for ( let test_num_attempt = 1; test_num_attempt <= num_to_make; test_num_attempt++ ) {
|
|
326
|
+
misfits.reset_warnings();
|
|
327
|
+
|
|
328
|
+
// Example of unique text: "(1 of 2)"
|
|
329
|
+
let { unqiue_Scenario_lines } = misfits.add(
|
|
330
|
+
get_definitely_unqiue_Scenario_lines({
|
|
331
|
+
scenario,
|
|
332
|
+
generator_lines,
|
|
333
|
+
test_num: actual_test_num,
|
|
334
|
+
indexes_to_delete
|
|
335
|
+
})
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
if ( scenario.steps.length === 0 ) { break; } // Discuss: Can this happen here?
|
|
339
|
+
|
|
340
|
+
let { middle_lines, final_significant_Step_index } = misfits.add(
|
|
341
|
+
this.try_to_get_one_unique_middle({
|
|
342
|
+
scenario, generator_lines, indexes_to_delete,
|
|
343
|
+
})
|
|
344
|
+
);
|
|
345
|
+
if ( misfits.errors.length > 0 ) { break; }
|
|
346
|
+
if ( middle_lines.length === 0 ) { continue; }
|
|
347
|
+
|
|
348
|
+
let { final_lines } = misfits.add(
|
|
349
|
+
get_final_lines({
|
|
350
|
+
generator_lines, final_significant_Step_index, indexes_to_delete
|
|
351
|
+
})
|
|
352
|
+
);
|
|
353
|
+
if ( misfits.errors.length > 0 ) { break; }
|
|
354
|
+
|
|
355
|
+
new_test_bodies.push( put_together_new_test_text({
|
|
356
|
+
starting_lines: unqiue_Scenario_lines,
|
|
357
|
+
middle_lines,
|
|
358
|
+
final_lines,
|
|
359
|
+
warnings: misfits.warnings
|
|
360
|
+
}) );
|
|
361
|
+
|
|
362
|
+
actual_test_num++;
|
|
363
|
+
// Discuss: Break if a test was skipped
|
|
364
|
+
|
|
365
|
+
} // End for desired num_tests
|
|
366
|
+
|
|
367
|
+
if ( misfits.errors.length > 0 ) {
|
|
368
|
+
return { new_Scenarios_text: ``, ...misfits };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
let got_fewer_tests_than_desired = new_test_bodies.length < num_to_make;
|
|
372
|
+
if ( got_fewer_tests_than_desired ) {
|
|
373
|
+
let warn_too_few = log.warn({ code: `ALK0260`, context: `generating files generator-scenario-level`, },
|
|
374
|
+
`ALKiln could only make ${ new_test_bodies.length } *unique* tests out of the ${ num_to_make } the author wanted for this Scenario. Note that ALKiln compares all generated Scenarios with each other to make sure all are unique. This Scenario may be a duplicate of a Scenario generated by a previous generator. The Scenario:\n ${ scenario_description_for_logs({ scenario }) }`,
|
|
375
|
+
)
|
|
376
|
+
// Discuss: Should we add more structure to Misbehaviors to give them
|
|
377
|
+
// context levels (Feature, generator Scenario, generated Scenario)
|
|
378
|
+
misfits.add({ warnings: [ warn_too_few ] });
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
let new_Scenarios_text = new_test_bodies.join(`\n\n`);
|
|
382
|
+
new_Scenarios_text = add_total_tests({
|
|
383
|
+
scenario_group_str: new_Scenarios_text, num_tests: new_test_bodies.length
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
log.debug({ code: `ALK0261`, context: `generating files`, }, `new_Scenarios_text before adding warnings:\n`, new_Scenarios_text );
|
|
387
|
+
|
|
388
|
+
return { new_Scenarios_text, ...misfits };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
try_to_get_one_unique_middle({
|
|
392
|
+
scenario,
|
|
393
|
+
generator_lines,
|
|
394
|
+
indexes_to_delete
|
|
395
|
+
}) {
|
|
396
|
+
/** Return data for new lines that should be a unique new test body.
|
|
397
|
+
*
|
|
398
|
+
* @param {object} obj - Named arguments
|
|
399
|
+
* @param {object} obj.scenario - Gherkin Scenario object
|
|
400
|
+
* @param {[str]} obj.generator_lines - Lines in the generator file
|
|
401
|
+
* @param {[int]} obj.indexes_to_delete - Lines to avoid adding to the new
|
|
402
|
+
* file
|
|
403
|
+
*
|
|
404
|
+
* @returns {object} rtn - Named return values
|
|
405
|
+
* @returns {[str]} rtn.middle_lines - 0 or more strings for the new test
|
|
406
|
+
* @returns {int} rtn.final_significant_Step_index - Generator file index
|
|
407
|
+
* for the final found significant Step
|
|
408
|
+
* @returns {[str]} rtn.warnings - 0 or more warning messages.
|
|
409
|
+
* @returns {[Error|str]} rtn.errors - 0 or more strings or Errors
|
|
410
|
+
* */
|
|
411
|
+
let misfits = new Misbehaviors();
|
|
412
|
+
|
|
413
|
+
log.debug({ code: `ALK0249`, context: `generating files`, }, `Trying to get unique middle` );
|
|
414
|
+
|
|
415
|
+
let keyword_index = get_keyword_index({ scenario });
|
|
416
|
+
|
|
417
|
+
// Try to make one unique test
|
|
418
|
+
// Accumulators and state trackers we'll use later
|
|
419
|
+
let attempt_lines = [];
|
|
420
|
+
let prev_significant_Step_index = keyword_index;
|
|
421
|
+
function clear_inner_state() {
|
|
422
|
+
/** Reset scope variables to build new unique test or clear out values */
|
|
423
|
+
attempt_lines = [];
|
|
424
|
+
prev_significant_Step_index = keyword_index;
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
// Loop management
|
|
428
|
+
let attempts = { count: 0, max: 10 };
|
|
429
|
+
let is_unique = false;
|
|
430
|
+
while ( !is_unique && attempts.count < attempts.max ) {
|
|
431
|
+
|
|
432
|
+
attempts.count++;
|
|
433
|
+
misfits.reset_warnings();
|
|
434
|
+
|
|
435
|
+
({ attempt_lines, prev_significant_Step_index } = misfits.add(
|
|
436
|
+
get_possibly_unique_lines_data({
|
|
437
|
+
scenario,
|
|
438
|
+
generator_lines,
|
|
439
|
+
indexes_to_delete
|
|
440
|
+
})
|
|
441
|
+
));
|
|
442
|
+
|
|
443
|
+
if ( misfits.errors.length > 0 ) {
|
|
444
|
+
clear_inner_state(); // Clear data before leaving loop
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
let one_test_body = attempt_lines.join(`\n`);
|
|
449
|
+
log.debug({ code: `ALK0257`, context: `generating files` }, `Generated test:\n${ one_test_body }` );
|
|
450
|
+
|
|
451
|
+
let is_duplicate = this.all_Scenarios_bodies_map[ one_test_body ];
|
|
452
|
+
if ( is_duplicate ) {
|
|
453
|
+
log.debug({ code: `ALK0258`, context: `generating files` }, `Test is a duplicate on attempt ${ attempts.count }.` );
|
|
454
|
+
if ( attempts.count >= attempts.max ) { clear_inner_state(); }
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Otherwise, add to the global map and finish
|
|
459
|
+
log.debug({ code: `ALK0259`, context: `generating files` }, `This test is unique on attempt number ${ attempts.count }.` );
|
|
460
|
+
is_unique = true;
|
|
461
|
+
this.all_Scenarios_bodies_map[ one_test_body ] = true;
|
|
462
|
+
|
|
463
|
+
} // ends while generated scenario is not unique or pass max attempts
|
|
464
|
+
|
|
465
|
+
return {
|
|
466
|
+
middle_lines: attempt_lines,
|
|
467
|
+
final_significant_Step_index: prev_significant_Step_index,
|
|
468
|
+
...misfits
|
|
469
|
+
};
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
}; // Ends TestGenerator {}
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
function get_generator_AST({ file_text, generator_path }) {
|
|
476
|
+
/** Return any errors created when Gherkin tries to parse the generator file.
|
|
477
|
+
* If there are any errors, throw a list of all the errors.
|
|
478
|
+
*
|
|
479
|
+
* @param {object} obj - Named arguments
|
|
480
|
+
* @param {string} obj.file_text - Text of the Gherkin file
|
|
481
|
+
* @param {string} obj.generator_path - Path to the generator file
|
|
482
|
+
*
|
|
483
|
+
* @returns {object} rtn - Named return values
|
|
484
|
+
* @returns {object} rtn.doc_AST - Gherkin AST of a .feature file
|
|
485
|
+
* */
|
|
486
|
+
let { AST: doc_AST, errors: generator_errors } = get_Gherkin_AST({ file_text });
|
|
487
|
+
|
|
488
|
+
if ( generator_errors.length <= 0 && doc_AST.feature ) {
|
|
489
|
+
return { doc_AST, errors: [] };
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/** Error handling */
|
|
493
|
+
|
|
494
|
+
if ( generator_errors.length > 0 ) {
|
|
495
|
+
let syntax_error = log.error({ code: `ALK0232`, context: `generating files`, before: `\n\n`, },
|
|
496
|
+
`The author's generator .feature file at "${ generator_path }" has Gherkin syntax error(s). You might see more warnings above or below this one.\n`,
|
|
497
|
+
...get_error_stacks({ errors: generator_errors })
|
|
498
|
+
);
|
|
499
|
+
return { doc_AST: null, errors: [new Error( syntax_error )] };
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// We have never been able to trigger `!doc_AST`
|
|
503
|
+
|
|
504
|
+
if ( !doc_AST.feature ) {
|
|
505
|
+
let feature_error = log.error({ code: `ALK0233`, context: `generating files`, before: `\n\n`, },
|
|
506
|
+
`The author's generator .feature file at "${ generator_path }" needs to include the \`Feature\` keyword.`
|
|
507
|
+
);
|
|
508
|
+
return { doc_AST: null, errors: [new Error( feature_error )] };
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Should not get here
|
|
512
|
+
let unknown_error = log.error({ code: `ALK0234`, context: `generating files`, before: `\n\n`, },
|
|
513
|
+
`The author's generator .feature file at "${ generator_path }" ran into an unanticipated error when ALKiln tried to parse it. If you have a moment, we would love for you to send us your debug.txt.`
|
|
514
|
+
);
|
|
515
|
+
return { doc_AST, errors: [new Error( unknown_error )] };
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
function get_Feature_text({ feature, generator_lines }) {
|
|
520
|
+
/** Return text of Feature section to put at the top of the new file
|
|
521
|
+
*
|
|
522
|
+
* @param {object} obj - Named arguments
|
|
523
|
+
* @param {object} obj.feature - Gherkin AST `feature` object
|
|
524
|
+
* @param {[string]} obj.generator_lines - Text of the generator file as a
|
|
525
|
+
* list
|
|
526
|
+
*
|
|
527
|
+
* @returns {string} - Text of the new file's Feature section
|
|
528
|
+
* */
|
|
529
|
+
add_Feature_randomization_indicators({ generator_lines, feature });
|
|
530
|
+
|
|
531
|
+
let first_scenario = feature.children[0].scenario;
|
|
532
|
+
let first_Scenario_line = get_Scenario_start_line_num({ scenario: first_scenario });
|
|
533
|
+
let first_Scenario_file_index = as_index( first_Scenario_line );
|
|
534
|
+
let index_of_last_line = first_Scenario_file_index - 1;
|
|
535
|
+
|
|
536
|
+
let feature_text = generator_lines.slice(
|
|
537
|
+
0, index_of_last_line + 1 // `slice` excludes the last index
|
|
538
|
+
).join(`\n`);
|
|
539
|
+
|
|
540
|
+
return feature_text;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
function add_Feature_randomization_indicators({ feature={}, generator_lines=`` }) {
|
|
545
|
+
/** Mutate `generator_lines`, modify the tag and name of the Feature section.
|
|
546
|
+
*
|
|
547
|
+
* @param {object} obj - Named arguments
|
|
548
|
+
* @param {object} obj.feature - Gherkin AST `feature` object
|
|
549
|
+
* @param {[string]} obj.generator_lines - Text of the generator file as a
|
|
550
|
+
* list
|
|
551
|
+
*
|
|
552
|
+
* @returns {undefined}
|
|
553
|
+
* */
|
|
554
|
+
let feature_index = as_index( feature.location.line );
|
|
555
|
+
|
|
556
|
+
let tag_indicator = `@alkiln @generated @randomized @constraints`;
|
|
557
|
+
let name_indicator = `(generated by ALKiln)`;
|
|
558
|
+
let new_f_name_line = ``;
|
|
559
|
+
|
|
560
|
+
if ( feature.tags.length > 0 ) {
|
|
561
|
+
// Modify existing tags
|
|
562
|
+
let tags_index = as_index( feature.tags[0].location.line );
|
|
563
|
+
let tags_starter = generator_lines[ tags_index ];
|
|
564
|
+
|
|
565
|
+
generator_lines[ tags_index ] = `${ tags_starter } ${ tag_indicator }`;
|
|
566
|
+
new_f_name_line = generator_lines[ feature_index ];
|
|
567
|
+
|
|
568
|
+
} else {
|
|
569
|
+
// If no existing tags, add new tag line
|
|
570
|
+
new_f_name_line = `${ tag_indicator }\n${ generator_lines[ feature_index ] }`;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
generator_lines[ feature_index ] = `${ new_f_name_line } ${ name_indicator }`;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
function get_Scenario_start_line_num({ scenario }) {
|
|
578
|
+
/** Returns the Gherkin line number where the Scenario really starts. That
|
|
579
|
+
* could be where the tags are or where the Scenario keyword is.
|
|
580
|
+
*
|
|
581
|
+
* @param {object} obj - Named arguments
|
|
582
|
+
* @param {object} obj.scenario - The Gherkin AST for a Scenario
|
|
583
|
+
*
|
|
584
|
+
* @returns {int} - Gherkin line number where the Scenario starts
|
|
585
|
+
* */
|
|
586
|
+
if ( scenario.tags.length > 0 ) {
|
|
587
|
+
return scenario.tags[0].location.line;
|
|
588
|
+
} else {
|
|
589
|
+
return scenario.location.line;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
function get_fore_Steps({ scenario }) {
|
|
595
|
+
/** Get Steps that must come in the first section of any kind of Scenario that
|
|
596
|
+
* ALKiln manipulates before running the cucumber tests. Does this really
|
|
597
|
+
* need to be abstracted for possible future "first Step"s already?
|
|
598
|
+
*
|
|
599
|
+
* Discuss: If a "first" Step appears multiple times, should that be an error
|
|
600
|
+
* or a warning?
|
|
601
|
+
*
|
|
602
|
+
* This code may be premature. We currently only have one type of fore-Step,
|
|
603
|
+
* though the author is allowed to make multiple of that kind without
|
|
604
|
+
* causing an error.
|
|
605
|
+
*
|
|
606
|
+
* @param {object} obj - Named arguments
|
|
607
|
+
* @param {object} obj.scenario - Gherkin AST `scenario` object
|
|
608
|
+
*
|
|
609
|
+
* @returns {object} rtn - Named return values
|
|
610
|
+
* @returns {int} rtn.num_to_make - Number of tests to generate
|
|
611
|
+
* @returns {[int]} rtn.file_lines_to_delete - Which lines in the file to
|
|
612
|
+
* exclude when creating the new Scenario
|
|
613
|
+
* @returns {[str]} rtn.warnings - 0 or more warning messages.
|
|
614
|
+
* @returns {[Error|str]} rtn.errors - 0 or more strings or Errors
|
|
615
|
+
* */
|
|
616
|
+
let misfits = new Misbehaviors();
|
|
617
|
+
let steps = scenario.steps;
|
|
618
|
+
|
|
619
|
+
let first_vals = {
|
|
620
|
+
num_to_make: null,
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
let last_valid_fore_step_index = -1;
|
|
624
|
+
let file_lines_to_delete = [];
|
|
625
|
+
|
|
626
|
+
for ( let step_i = 0; step_i < steps.length; step_i++ ) {
|
|
627
|
+
let step = steps[ step_i ];
|
|
628
|
+
|
|
629
|
+
let all_results = [];
|
|
630
|
+
|
|
631
|
+
/** Number of tests to generate */
|
|
632
|
+
let { num_to_make } = misfits.add(
|
|
633
|
+
get_number_of_Scenarios_to_generate2({
|
|
634
|
+
step,
|
|
635
|
+
prev_num_to_make: first_vals.num_to_make
|
|
636
|
+
})
|
|
637
|
+
);
|
|
638
|
+
first_vals.num_to_make = num_to_make || first_vals.num_to_make;
|
|
639
|
+
if ( num_to_make !== null ) {
|
|
640
|
+
file_lines_to_delete.push(as_index( step.location.line ));
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/** Other possible fore Steps */
|
|
644
|
+
// TBS
|
|
645
|
+
|
|
646
|
+
/** Possibly the end */
|
|
647
|
+
// If none of the "first Step" values were satisfied, stop here
|
|
648
|
+
if ( [ num_to_make ].every(function ( item ) { return item === null; }) ) {
|
|
649
|
+
break;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
last_valid_fore_step_index = step_i;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
let there_were_no_fore_Steps = last_valid_fore_step_index === -1;
|
|
656
|
+
let description = scenario_description_for_logs({ scenario });
|
|
657
|
+
if ( there_were_no_fore_Steps ) {
|
|
658
|
+
misfits.add({ errors: [
|
|
659
|
+
new Error( log.error({ code: `ALK0242`, context: `generating files`, },
|
|
660
|
+
`A generator Scenario's first Step must say how many constrained random answers tests to make. The generator Scenario on line ${ scenario.location.line } is missing that Step. The Scenario's description is:\n${ description }`
|
|
661
|
+
) )
|
|
662
|
+
] });
|
|
663
|
+
} else {
|
|
664
|
+
log.debug({ code: `ALK0243`, context: `generating files` },
|
|
665
|
+
`The final "first Step" Gherkin index was ${ last_valid_fore_step_index }. The final file index of that Step was ${ as_index(steps[ last_valid_fore_step_index ].location.line) }. The Scenario description:\n${ description }`
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if ( first_vals.num_to_make ) {
|
|
670
|
+
log.debug({ code: `ALK0244`, context: `generating files` },
|
|
671
|
+
`Will try to make ${ first_vals.num_to_make } tests.`
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
return {
|
|
676
|
+
num_to_make: first_vals.num_to_make,
|
|
677
|
+
file_lines_to_delete,
|
|
678
|
+
...misfits
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
const ALKILN_REGEX = /ALKiln/i;
|
|
684
|
+
const CONSTRAIN_REGEX = /constrain/i; // constrained/constraint/constraints
|
|
685
|
+
const RAND_REGEX = /rand/i; // random/randomness/randomized
|
|
686
|
+
const GENERAT_REGEX = /generat|create/i; // generate/generates/generated/generating/create/creates
|
|
687
|
+
const MAKE_REGEX = /make|made/i; // make/makes/made/making
|
|
688
|
+
const CAPTURE_NUMBERS_REGEX = /(\d+)/g;
|
|
689
|
+
function get_number_of_Scenarios_to_generate2({ step, prev_num_to_make }) {
|
|
690
|
+
/** Return null or a constraints Step.
|
|
691
|
+
*
|
|
692
|
+
* @param {object} obj - Named arguments
|
|
693
|
+
* @param {object} obj.step - A valid constraints Step Gherkin object
|
|
694
|
+
* @param {int|undefined} obj.prev_num_to_make - A previously found number
|
|
695
|
+
* if there was one, or `undefined`
|
|
696
|
+
*
|
|
697
|
+
* @returns {null|object} `null` or a constraints Step
|
|
698
|
+
* */
|
|
699
|
+
// Given ALKiln should make 10 constrained random tests
|
|
700
|
+
let warnings = [];
|
|
701
|
+
let errors = [];
|
|
702
|
+
let text = step.text;
|
|
703
|
+
let user_num = null;
|
|
704
|
+
|
|
705
|
+
// Use `.test()` without `g` flag. See
|
|
706
|
+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test#using_test_on_a_regex_with_the_global_flag
|
|
707
|
+
let is_num_Step = ALKILN_REGEX.test( text )
|
|
708
|
+
&& CONSTRAIN_REGEX.test( text )
|
|
709
|
+
&& ( RAND_REGEX.test( text )
|
|
710
|
+
|| GENERAT_REGEX.test( text )
|
|
711
|
+
|| MAKE_REGEX.test( text )
|
|
712
|
+
);
|
|
713
|
+
|
|
714
|
+
/** Not a numbers Step, which errors elsewhere if needed */
|
|
715
|
+
if ( !is_num_Step ) {
|
|
716
|
+
return { num_to_make: null, warnings, errors };
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/** Conflicts with previous number Step */
|
|
720
|
+
if ( prev_num_to_make ) {
|
|
721
|
+
// Discuss: If there are multiple number Steps, the author ends up seeing
|
|
722
|
+
// multiple messages about what ALKiln will use to set the number and
|
|
723
|
+
// also possibly repeat this message. Can we do something about that in
|
|
724
|
+
// a way that is also maintainable?
|
|
725
|
+
warnings.push(
|
|
726
|
+
log.warn({ code: `ALK0238`, context: `generating files`, },
|
|
727
|
+
`A generator Scenario has too many Steps that set the number of tests to make. ALKiln will use the final number Step it finds or calculates. The current Step:\n ${ text }`
|
|
728
|
+
)
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
let number_matcher = text.match( CAPTURE_NUMBERS_REGEX );
|
|
733
|
+
|
|
734
|
+
/** Missing number */
|
|
735
|
+
if ( number_matcher === null ) {
|
|
736
|
+
warnings.push(
|
|
737
|
+
log.warn({ code: `ALK0239`, context: `generating files`, },
|
|
738
|
+
`A generator Scenario is missing the number of tests to make. ALKiln will now make 1 constrained random answers test. The current Step:\n ${ text }`
|
|
739
|
+
)
|
|
740
|
+
);
|
|
741
|
+
return { num_to_make: 1, warnings, errors };
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/** Too many numbers */
|
|
745
|
+
if ( number_matcher && number_matcher.length > 1 ) {
|
|
746
|
+
// Some feedback indicated using the last number makes more sense to people
|
|
747
|
+
// than the first
|
|
748
|
+
user_num = parseInt( number_matcher[ number_matcher.length - 1 ]);
|
|
749
|
+
warnings.push(
|
|
750
|
+
log.warn({ code: `ALK0240`, context: `generating files`, },
|
|
751
|
+
`This constrained random answers Step is ambiguous because it has more than 1 number. ALKiln will now use the last number in the Step (${ user_num }). The Step:\n ${ text }`,
|
|
752
|
+
)
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/** Happy path */
|
|
757
|
+
if ( number_matcher && !user_num ) {
|
|
758
|
+
user_num = parseInt( number_matcher[ 0 ]);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
let num_to_make = Math.min(
|
|
762
|
+
session_vars.get_max_tests_per_generator_scenario(),
|
|
763
|
+
user_num
|
|
764
|
+
);
|
|
765
|
+
|
|
766
|
+
if ( num_to_make !== user_num ) {
|
|
767
|
+
warnings.push(
|
|
768
|
+
log.warn({ code: `ALK0241`, context: `generating files`, },
|
|
769
|
+
`The author asked to make ${ user_num } tests, but maximum allowed is ${ num_to_make }. ALKiln will now make ${ num_to_make } tests. 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.`
|
|
770
|
+
)
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
return { num_to_make, warnings, errors };
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
function get_definitely_unqiue_Scenario_lines({
|
|
779
|
+
scenario,
|
|
780
|
+
generator_lines,
|
|
781
|
+
test_num,
|
|
782
|
+
indexes_to_delete
|
|
783
|
+
}) {
|
|
784
|
+
/** Return the lines in the Scenario that will definitely be unique - they
|
|
785
|
+
* have counters and such.
|
|
786
|
+
*
|
|
787
|
+
* Example of unique text: `(1 of 2)`
|
|
788
|
+
*
|
|
789
|
+
* @param {object} obj - Named arguments
|
|
790
|
+
* @param {Gherkin object} obj.scenario - Gherkin AST scenario object
|
|
791
|
+
* @param {[str]} obj.generator_lines - Full list of strings from the
|
|
792
|
+
* generator file.
|
|
793
|
+
* @param {int} obj.test_num - What number test this is out of all the tests
|
|
794
|
+
* we need to generate for this scenario
|
|
795
|
+
* @param {[int]} obj.indexes_to_delete - Lines to avoid adding to the new
|
|
796
|
+
* file
|
|
797
|
+
*
|
|
798
|
+
* @returns {object} rtn - Named return values
|
|
799
|
+
* @returns {[str]} rtn.unqiue_Scenario_lines - As the name says
|
|
800
|
+
* @returns {[str]} rtn.warnings - 0 or more warning messages.
|
|
801
|
+
* @returns {[Error|str]} rtn.errors - 0 or more strings or Errors
|
|
802
|
+
* */
|
|
803
|
+
let misfits = new Misbehaviors();
|
|
804
|
+
|
|
805
|
+
let unqiue_Scenario_lines = [];
|
|
806
|
+
|
|
807
|
+
log.debug({ code: `ALK0245`, context: `generating files`, },
|
|
808
|
+
`Trying to get get_definitely_unqiue_Scenario_lines`
|
|
809
|
+
);
|
|
810
|
+
|
|
811
|
+
let tags_index = get_tag_index({ scenario });
|
|
812
|
+
unqiue_Scenario_lines.push( get_tag_text({
|
|
813
|
+
generator_lines, tags_index, test_num
|
|
814
|
+
}) );
|
|
815
|
+
|
|
816
|
+
let keyword_index = get_keyword_index({ scenario });
|
|
817
|
+
let keyword_text = get_keyword_text({
|
|
818
|
+
generator_lines, keyword_index, test_num
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
let tags_tweens = [];
|
|
822
|
+
if ( tags_index !== null ) {
|
|
823
|
+
let { tweens } = misfits.add(
|
|
824
|
+
get_tweens({
|
|
825
|
+
end_index_of_prev: tags_index,
|
|
826
|
+
start_index_of_next: keyword_index,
|
|
827
|
+
generator_lines: generator_lines,
|
|
828
|
+
indexes_to_delete
|
|
829
|
+
})
|
|
830
|
+
);
|
|
831
|
+
tags_tweens = tweens;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
unqiue_Scenario_lines.push( ...tags_tweens, keyword_text );
|
|
835
|
+
|
|
836
|
+
log.debug({ code: `ALK0248`, context: `generating files`, },
|
|
837
|
+
`Unique lines:`, unqiue_Scenario_lines
|
|
838
|
+
);
|
|
839
|
+
|
|
840
|
+
return { unqiue_Scenario_lines, ...misfits };
|
|
841
|
+
};
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
function get_tag_index({ scenario }) {
|
|
845
|
+
/** Return the generator file index of the Scenario's tag line or null if the
|
|
846
|
+
* tag line is non-existent
|
|
847
|
+
*
|
|
848
|
+
* @param {object} obj - Named arguments
|
|
849
|
+
* @param {object} obj.scenario - Gherkin Scenario object
|
|
850
|
+
*
|
|
851
|
+
* @returns {null|int} - null or tag line index
|
|
852
|
+
* */
|
|
853
|
+
if ( scenario.tags.length > 0 ) {
|
|
854
|
+
return as_index( scenario.tags[0].location.line );
|
|
855
|
+
}
|
|
856
|
+
return null;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
|
|
860
|
+
const TOTAL_TESTS_FOR_THIS_SCENARIO_PLACEHOLDER = `ALKILN_TOTAL_TESTS_PLACEHOLDER`;
|
|
861
|
+
const TOTAL_TESTS_REGEX = new RegExp( TOTAL_TESTS_FOR_THIS_SCENARIO_PLACEHOLDER, `g` );
|
|
862
|
+
const WARNING_TAG_PLACEHOLDER = `ALKILN_MAYBE_WARNING_TAG_PLACEHOLDER`;
|
|
863
|
+
function get_tag_text({ generator_lines=[], tags_index=null, test_num=1 }) {
|
|
864
|
+
/** Edit or create the tag line of the new Scenario to include test-specific
|
|
865
|
+
* information
|
|
866
|
+
*
|
|
867
|
+
* @param {object} obj - Named arguments
|
|
868
|
+
* @param {[str]} obj.generator_lines - Full list of strings from the
|
|
869
|
+
* generator file.
|
|
870
|
+
* @param {int} obj.tags_index - Index of the keyword line
|
|
871
|
+
* @param {int} obj.test_num - Which iteration of the test this is
|
|
872
|
+
*
|
|
873
|
+
* @returns {str} - New text for the tag line of the new Scenario
|
|
874
|
+
* */
|
|
875
|
+
let tags = [];
|
|
876
|
+
if ( tags_index !== null ) {
|
|
877
|
+
let tags_as_the_author_wrote_them = generator_lines[ tags_index ];
|
|
878
|
+
tags.push( tags_as_the_author_wrote_them );
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
tags.push([ `@scenario${ test_num }_of_${ TOTAL_TESTS_FOR_THIS_SCENARIO_PLACEHOLDER }${ WARNING_TAG_PLACEHOLDER }` ]);
|
|
882
|
+
return tags.join(` `);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
// Note: `get_keyword_index()` is used repeatedly and thus is at the bottom
|
|
887
|
+
|
|
888
|
+
|
|
889
|
+
function get_keyword_text({ generator_lines=[], keyword_index=null, test_num=1 }) {
|
|
890
|
+
/** Edit the text in the keyword line to include test-specific information.
|
|
891
|
+
*
|
|
892
|
+
* @param {object} obj - Named arguments
|
|
893
|
+
* @param {[str]} obj.generator_lines - Full list of strings from the
|
|
894
|
+
* generator file.
|
|
895
|
+
* @param {int} obj.keyword_index - Index of the keyword line
|
|
896
|
+
* @param {int} obj.test_num - Which iteration of the test this is
|
|
897
|
+
*
|
|
898
|
+
* @returns {str} - New text for the keyword line
|
|
899
|
+
* */
|
|
900
|
+
let keyword_text = `${ generator_lines[ keyword_index ] } (${ test_num } of ${ TOTAL_TESTS_FOR_THIS_SCENARIO_PLACEHOLDER })`;
|
|
901
|
+
return keyword_text;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
function get_possibly_unique_lines_data({
|
|
906
|
+
scenario,
|
|
907
|
+
generator_lines,
|
|
908
|
+
indexes_to_delete
|
|
909
|
+
}) {
|
|
910
|
+
/** Return a list of lines and other data for one randomized test.
|
|
911
|
+
*
|
|
912
|
+
* @param {object} obj - Named arguments
|
|
913
|
+
* @param {object} obj.scenario - Gherkin Scenario object
|
|
914
|
+
* @param {[str]} obj.generator_lines - Lines in the generator file
|
|
915
|
+
* @param {[int]} obj.indexes_to_delete - Lines to avoid adding to the new
|
|
916
|
+
* file
|
|
917
|
+
*
|
|
918
|
+
* @returns {object} rtn - Named return values
|
|
919
|
+
* @returns {[str]} rtn.attempt_lines - 0 or more possibly unique lines for
|
|
920
|
+
* the new test
|
|
921
|
+
* @returns {int} rtn.final_significant_Step_index - Generator file index for
|
|
922
|
+
* the final found significant Step
|
|
923
|
+
* @returns {[str]} rtn.warnings - 0 or more warning messages.
|
|
924
|
+
* @returns {[Error|str]} rtn.errors - 0 or more strings or Errors
|
|
925
|
+
* */
|
|
926
|
+
let misfits = new Misbehaviors();
|
|
927
|
+
|
|
928
|
+
let attempt_lines = [];
|
|
929
|
+
let prev_significant_Step_index = get_keyword_index({ scenario });
|
|
930
|
+
let found_at_least_1_significant_step = false;
|
|
931
|
+
for ( let step of scenario.steps ) {
|
|
932
|
+
|
|
933
|
+
let is_significant = is_constraints_Step({ step });
|
|
934
|
+
if ( !is_significant ) {
|
|
935
|
+
continue;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
found_at_least_1_significant_step = true;
|
|
939
|
+
|
|
940
|
+
// Get the lines since the last line we got (comments/blanks/etc)
|
|
941
|
+
let significant_Step_start_index = as_index( step.location.line );
|
|
942
|
+
let { tweens } = misfits.steal_misbehaviors(
|
|
943
|
+
get_tweens({
|
|
944
|
+
end_index_of_prev: prev_significant_Step_index,
|
|
945
|
+
start_index_of_next: significant_Step_start_index,
|
|
946
|
+
generator_lines,
|
|
947
|
+
indexes_to_delete
|
|
948
|
+
})
|
|
949
|
+
); // If error, keep gathering errors
|
|
950
|
+
|
|
951
|
+
let { table: story_table_str } = misfits.add(
|
|
952
|
+
get_random_Story_Table({ step, generator_lines })
|
|
953
|
+
);
|
|
954
|
+
|
|
955
|
+
if ( misfits.errors.length > 0 ) { break; }
|
|
956
|
+
attempt_lines.push( ...tweens, story_table_str );
|
|
957
|
+
|
|
958
|
+
prev_significant_Step_index = get_final_file_index_of_constraints_or_fore_Step({ step });
|
|
959
|
+
} // Ends loop for every step
|
|
960
|
+
|
|
961
|
+
if ( !found_at_least_1_significant_step ) {
|
|
962
|
+
let error = log.error({ code: `ALK0256`, context: `generating files`, },
|
|
963
|
+
`ALKiln found 0 valid constraints Steps for the Scenario at line ${ scenario.location.line }: "${ scenario_description_for_logs({ scenario }) }". Check the Step docs at https://assemblyline.suffolklitlab.org/docs/alkiln/writing#constrained-random-answers for the correct language and syntax.`
|
|
964
|
+
);
|
|
965
|
+
misfits.add({ errors: [new Error( error )] });
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
return {
|
|
969
|
+
attempt_lines,
|
|
970
|
+
prev_significant_Step_index,
|
|
971
|
+
...misfits
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
|
|
976
|
+
const GET_TO_REGEX = /get|gets|end|reach|stop/i;
|
|
977
|
+
const WITH_REGEX = /with|when/i;
|
|
978
|
+
const PICK_REGEX = /pick|choose/i;
|
|
979
|
+
function is_constraints_Step({ step }) {
|
|
980
|
+
/** Return `true` if the Step is a constraints Step, otherwise return `false`.
|
|
981
|
+
*
|
|
982
|
+
* @param {object} obj - Named arguments
|
|
983
|
+
* @param {Gherkin object} obj.step - A valid constraints Step Gherkin object
|
|
984
|
+
*
|
|
985
|
+
* @returns {bool} `true` if it's a constraints Step, otherwise false
|
|
986
|
+
* */
|
|
987
|
+
// Use `.test()` without `g` flag. See
|
|
988
|
+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test#using_test_on_a_regex_with_the_global_flag
|
|
989
|
+
let matches_constraints_Step = ALKILN_REGEX.test( step.text )
|
|
990
|
+
&& (
|
|
991
|
+
GET_TO_REGEX.test( step.text )
|
|
992
|
+
|| PICK_REGEX.test( step.text )
|
|
993
|
+
);
|
|
994
|
+
return matches_constraints_Step;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
|
|
998
|
+
function get_random_Story_Table({
|
|
999
|
+
step, generator_lines, indexes_to_delete
|
|
1000
|
+
}) {
|
|
1001
|
+
/** Return a randomized Story Table string.
|
|
1002
|
+
*
|
|
1003
|
+
* TODO: Abstract this further
|
|
1004
|
+
*
|
|
1005
|
+
* @param {object} obj - Named arguments
|
|
1006
|
+
* @param {string} obj.ids - The ids this Story Table can reach
|
|
1007
|
+
* @param {object} obj.step - Gherkin AST of a Step
|
|
1008
|
+
* @param {[str]} obj.generator_lines - List of lines from the generator file
|
|
1009
|
+
*
|
|
1010
|
+
* @returns {object} rtn - Named return values
|
|
1011
|
+
* @returns {string} rtn.table - String of the whole Story Table Step
|
|
1012
|
+
* @returns {[str]} rtn.warnings - 0 or more warning messages.
|
|
1013
|
+
* @returns {[Error|str]} rtn.errors - 0 or more strings or Errors
|
|
1014
|
+
* */
|
|
1015
|
+
let misfits = new Misbehaviors();
|
|
1016
|
+
let story_table_parts = [];
|
|
1017
|
+
|
|
1018
|
+
let { ids } = misfits.add(
|
|
1019
|
+
get_final_ids({ step_text: step.text })
|
|
1020
|
+
);
|
|
1021
|
+
if ( misfits.errors.length > 0 ) { return { table: null, ...misfits }; }
|
|
1022
|
+
|
|
1023
|
+
if ( !step.dataTable ) {
|
|
1024
|
+
let error = log.error({ code: `ALK0251`, context: `generating files`, },
|
|
1025
|
+
`The table is missing for the constrained random answers Step at line ${ step.location.line }. The Step:`,
|
|
1026
|
+
get_flatter_table_Step({ step })
|
|
1027
|
+
);
|
|
1028
|
+
misfits.add({ errors: [ new Error( error ) ] });
|
|
1029
|
+
|
|
1030
|
+
} else if ( step.dataTable.rows.length === 1 ) {
|
|
1031
|
+
let error = log.error({ code: `ALK0252`, context: `generating files`, },
|
|
1032
|
+
`The body of the table is missing for the table of the constrained random answers Step at line ${ step.location.line }. There is only a header row. The Step:`,
|
|
1033
|
+
get_flatter_table_Step({ step }) );
|
|
1034
|
+
misfits.add({ errors: [new Error( error )] });
|
|
1035
|
+
|
|
1036
|
+
}
|
|
1037
|
+
if ( misfits.errors.length > 0 ) { return { table: null, ...misfits }; }
|
|
1038
|
+
|
|
1039
|
+
let step_indent = get_authors_indent({ located: step });
|
|
1040
|
+
let table_start = `${ step_indent }And I get to any of the question ids [${ ids.join(`, `) }] with this data:`;
|
|
1041
|
+
story_table_parts.push( table_start );
|
|
1042
|
+
|
|
1043
|
+
let step_bottom_index = as_index( step.location.line );
|
|
1044
|
+
let previous_significant_index = step_bottom_index;
|
|
1045
|
+
for ( let row_i = 0; row_i < step.dataTable.rows.length; row_i++ ) {
|
|
1046
|
+
|
|
1047
|
+
let row = step.dataTable.rows[ row_i ];
|
|
1048
|
+
|
|
1049
|
+
let row_file_index = as_index( row.location.line )
|
|
1050
|
+
let { tweens, errors } = misfits.add(
|
|
1051
|
+
get_tweens({
|
|
1052
|
+
end_index_of_prev: previous_significant_index,
|
|
1053
|
+
start_index_of_next: row_file_index,
|
|
1054
|
+
generator_lines: generator_lines,
|
|
1055
|
+
indexes_to_delete: []
|
|
1056
|
+
})
|
|
1057
|
+
);
|
|
1058
|
+
if ( misfits.errors.length > 0 ) { break; }
|
|
1059
|
+
story_table_parts.push( ...tweens );
|
|
1060
|
+
|
|
1061
|
+
if ( row_i === 0 ) {
|
|
1062
|
+
story_table_parts.push( get_table_header({ row }) );
|
|
1063
|
+
} else {
|
|
1064
|
+
story_table_parts.push( get_random_table_row({ row }) );
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
previous_significant_index = row_file_index;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
let table = story_table_parts.join(`\n`);
|
|
1071
|
+
log.debug({ code: `ALK0255`, context: `generating files`, }, `Table:`, table );
|
|
1072
|
+
return { table, ...misfits };
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
|
|
1076
|
+
let CAPTURE_QUESTION_IDS_REGEX = /("[^"]+")/g;
|
|
1077
|
+
function get_final_ids({ step_text }) {
|
|
1078
|
+
/** Return a list of docassemble question block ids (in double quotes) that
|
|
1079
|
+
* are in an ALKiln constraints Step. Every generated Story Table Step
|
|
1080
|
+
* must reach one of these ids.
|
|
1081
|
+
*
|
|
1082
|
+
* @param {object} obj - Named arguments
|
|
1083
|
+
* @param {string} obj.step_text - The text of a constraints Step
|
|
1084
|
+
*
|
|
1085
|
+
* Example:
|
|
1086
|
+
* get_final_ids({ step_text: `And I make 1 constrained random test that gets to "end1" or "end 2" with:` });
|
|
1087
|
+
* // [`"end1"`, `"end2"`]
|
|
1088
|
+
*
|
|
1089
|
+
* @returns {object} rtn - Named return values
|
|
1090
|
+
* @returns {["str"]} rtn.ids - 0 or more double-quoted docassemble question
|
|
1091
|
+
* block ids. Each generated Story Table Step must end at one of these
|
|
1092
|
+
* ids. this Scenario.
|
|
1093
|
+
* @returns {[str]} rtn.warnings - 0 or more warning messages.
|
|
1094
|
+
* @returns {[Error|str]} rtn.errors - 0 or more strings or Errors
|
|
1095
|
+
* */
|
|
1096
|
+
let errors = [];
|
|
1097
|
+
let warnings = [];
|
|
1098
|
+
|
|
1099
|
+
let ids = step_text.match( CAPTURE_QUESTION_IDS_REGEX );
|
|
1100
|
+
|
|
1101
|
+
if ( ids === null ) {
|
|
1102
|
+
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 }`;
|
|
1103
|
+
let error = log.error({ code: `ALK0250`, context: `generating files`, }, missing_ids_msg );
|
|
1104
|
+
errors.push(new Error( error ));
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
return { ids, warnings, errors };
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
|
|
1111
|
+
function get_flatter_table_Step({ step }) {
|
|
1112
|
+
/** Given a Gherkin `step` object, returns an object that is a less nested
|
|
1113
|
+
* version of the `step` object. If the Step doesn't have a table, return
|
|
1114
|
+
* the Step's text
|
|
1115
|
+
*
|
|
1116
|
+
* Yes, this is annoying to test, but it's a fairly simple way to show enough
|
|
1117
|
+
* info about the Step to be useful.
|
|
1118
|
+
*
|
|
1119
|
+
* Discuss: Should this go in the Log class?
|
|
1120
|
+
*
|
|
1121
|
+
* @param {object} obj - Named arguments
|
|
1122
|
+
* @param {object} obj.step - Valid Gherkin `step` object
|
|
1123
|
+
*
|
|
1124
|
+
* @returns {{ text, rows: [[str1, str2], ...] }} - Flatter version of `step`
|
|
1125
|
+
* */
|
|
1126
|
+
let simple = { text: step.text, rows: [] };
|
|
1127
|
+
|
|
1128
|
+
if ( !step.dataTable ) { return simple; }
|
|
1129
|
+
|
|
1130
|
+
for ( let row of step.dataTable.rows ) {
|
|
1131
|
+
let cells = [];
|
|
1132
|
+
for ( let cell of row.cells ) {
|
|
1133
|
+
cells.push( cell.value );
|
|
1134
|
+
}
|
|
1135
|
+
simple.rows.push( cells );
|
|
1136
|
+
}
|
|
1137
|
+
return simple;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
|
|
1141
|
+
function get_table_header({ row }) {
|
|
1142
|
+
/** Return the string for the Story Table header row. 2 or 3 column tables are
|
|
1143
|
+
* allowed.
|
|
1144
|
+
*
|
|
1145
|
+
* Discuss: Warn about wrong number of columns?
|
|
1146
|
+
*
|
|
1147
|
+
* @param {object} obj - Named arguments
|
|
1148
|
+
* @param {object} obj.row Valid Gherkin dataTable.row object
|
|
1149
|
+
*
|
|
1150
|
+
* @returns {string} A markdown table row with 2 or 3 columns
|
|
1151
|
+
* */
|
|
1152
|
+
let indent = get_authors_indent({ located: row });
|
|
1153
|
+
let num_cols = row.cells.length;
|
|
1154
|
+
let header = `${ indent }| var | value |`;
|
|
1155
|
+
if ( num_cols >= 3 ) { header += ` trigger |`; }
|
|
1156
|
+
return header;
|
|
1157
|
+
};
|
|
1158
|
+
|
|
1159
|
+
|
|
1160
|
+
function get_random_table_row({ row }) {
|
|
1161
|
+
/** Return the string of one Story Table row with a randomly chosen answer.
|
|
1162
|
+
*
|
|
1163
|
+
* @param {object} obj - Named arguments
|
|
1164
|
+
* @param {object} obj.row - Valid Gherkin dataTable.row object
|
|
1165
|
+
*
|
|
1166
|
+
* @returns {string} A markdown table row with 2 or 3 columns
|
|
1167
|
+
* */
|
|
1168
|
+
let indent = get_authors_indent({ located: row });
|
|
1169
|
+
let cells = row.cells;
|
|
1170
|
+
let num_cols = cells.length;
|
|
1171
|
+
|
|
1172
|
+
let answer = pick_random_answer({ answers_str: cells[1].value });
|
|
1173
|
+
|
|
1174
|
+
let row_str = `${ indent }| ${ cells[0].value } | ${ answer } |`;
|
|
1175
|
+
if ( cells.length >= 3 ) { row_str += ` ${ cells[2].value } |`; }
|
|
1176
|
+
return row_str;
|
|
1177
|
+
};
|
|
1178
|
+
|
|
1179
|
+
|
|
1180
|
+
function pick_random_answer({ answers_str=`` }) {
|
|
1181
|
+
/** Return one of the answers from the given string of possible answers.
|
|
1182
|
+
*
|
|
1183
|
+
* @param {object} obj - Named arguments
|
|
1184
|
+
* @param {str} obj.answers_str - A string representing one or more possible
|
|
1185
|
+
* answers. The possible answers are separated by this character
|
|
1186
|
+
* combination: `;;`
|
|
1187
|
+
*
|
|
1188
|
+
* Example:
|
|
1189
|
+
* pick_random_answer({ answers_str: `yes;; no;; maybe` });
|
|
1190
|
+
* // `no`
|
|
1191
|
+
*
|
|
1192
|
+
* TODO: Add a warning in the docs that Gherkin ignores whitespace at the
|
|
1193
|
+
* start and end of columns in a table.
|
|
1194
|
+
*
|
|
1195
|
+
* Discuss: let authors wrap content in backticks or some other delimiter.
|
|
1196
|
+
*
|
|
1197
|
+
* @returns {string} - One of the possible answers
|
|
1198
|
+
* */
|
|
1199
|
+
let choices = answers_str.replace(`;; `, `;;`).split(`;;`);
|
|
1200
|
+
if ( choices.includes(``) ) {
|
|
1201
|
+
// Discuss: Should we get rid of silent log warnings? They could help us
|
|
1202
|
+
// help authors troubleshoot their tests while not disturbing the author
|
|
1203
|
+
// with problems that may not exist. We can always make the warnings
|
|
1204
|
+
// louder later. Should we make them loud instead? And suggest the author
|
|
1205
|
+
// should put the empty one in the middle?
|
|
1206
|
+
log.debug({ code: `ALK0254`, context: `generating files` },
|
|
1207
|
+
`**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 }"`
|
|
1208
|
+
);
|
|
1209
|
+
}
|
|
1210
|
+
let choice_index = Math.floor( Math.random() * choices.length );
|
|
1211
|
+
return choices[ choice_index ];
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
|
|
1215
|
+
function get_final_file_index_of_constraints_or_fore_Step({ step }) {
|
|
1216
|
+
/** Returns the index of the last possible item in a Gherkin Step object. That
|
|
1217
|
+
* includes the file index of the last row in a `dataTable`.
|
|
1218
|
+
*
|
|
1219
|
+
* Research: How to get the final row of a Step with a multi-line string?
|
|
1220
|
+
* Answer: Maybe it's how to look for the end of any Step. Look for the
|
|
1221
|
+
* starting line of the next Step. Look above that to the first line that
|
|
1222
|
+
* isn't a comment or blank line (though the other way around).
|
|
1223
|
+
*
|
|
1224
|
+
* Research: How do find the line of the end of a Scenario considering a final
|
|
1225
|
+
* Step could be a multi-line quote or an examples table.
|
|
1226
|
+
*
|
|
1227
|
+
* Research: How to get the final row of an `Examples` table?
|
|
1228
|
+
* Answer: `scenario.examples` then `.tableHeader.cells` or `.tableBody.cells`
|
|
1229
|
+
* Maybe we need to go back to just parsing strings. That would make the
|
|
1230
|
+
* explanation easier too... On the other hand, we have to account for
|
|
1231
|
+
* other details, like empty lines or comment lines or who knows what else
|
|
1232
|
+
*
|
|
1233
|
+
* Research: When there's a repeated line in the Story Table, do we use the
|
|
1234
|
+
* first line we find or the last one? Then make the behavior in here
|
|
1235
|
+
* match that behavior. Or just leave it to its own devices.
|
|
1236
|
+
*
|
|
1237
|
+
* @param {object} obj - Named arguments
|
|
1238
|
+
* @param {object} obj.step - Gherkin AST `step` object
|
|
1239
|
+
*
|
|
1240
|
+
* @returns {int} - Index of the final line as an index in the generator file
|
|
1241
|
+
* */
|
|
1242
|
+
if ( !step.dataTable ) { return as_index( step.location.line ); }
|
|
1243
|
+
let rows = step.dataTable.rows;
|
|
1244
|
+
return as_index(rows[ rows.length - 1 ].location.line);
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
|
|
1248
|
+
function get_final_lines({
|
|
1249
|
+
generator_lines,
|
|
1250
|
+
final_significant_Step_index,
|
|
1251
|
+
indexes_to_delete
|
|
1252
|
+
}) {
|
|
1253
|
+
/** Return the rest of the lines till the end of the file
|
|
1254
|
+
*
|
|
1255
|
+
* Discuss: Alternatively, get all the remaining lines until the next Scenario
|
|
1256
|
+
* that are before comments and blank lines. Maybe that's not possible
|
|
1257
|
+
* given descriptions and docstrings (That might have the word "Scenario:"
|
|
1258
|
+
* in it). Still, may be able to combine Gherkin AST and parsing. to
|
|
1259
|
+
* achieve the result we want.
|
|
1260
|
+
*
|
|
1261
|
+
* @param {object} obj - Named arguments
|
|
1262
|
+
* @param {[str]} obj.generator_lines - List of lines from the generator file
|
|
1263
|
+
* @param {int} rtn.final_significant_Step_index - Generator file index for
|
|
1264
|
+
* the final found significant Step
|
|
1265
|
+
*
|
|
1266
|
+
* @returns {[str]} - 0 or more lines for the new file.
|
|
1267
|
+
* */
|
|
1268
|
+
|
|
1269
|
+
let { tweens, errors, warnings } = get_tweens({
|
|
1270
|
+
end_index_of_prev: final_significant_Step_index,
|
|
1271
|
+
start_index_of_next: generator_lines.length,
|
|
1272
|
+
generator_lines,
|
|
1273
|
+
indexes_to_delete
|
|
1274
|
+
});
|
|
1275
|
+
|
|
1276
|
+
return { final_lines: tweens, errors, warnings };
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
|
|
1280
|
+
function put_together_new_test_text({
|
|
1281
|
+
starting_lines, middle_lines, final_lines, warnings
|
|
1282
|
+
}) {
|
|
1283
|
+
/** Return the combined text with its final modifications
|
|
1284
|
+
*
|
|
1285
|
+
* @param {object} obj - Named arguments
|
|
1286
|
+
* @param {[str]} obj.starting_lines - 0 or more lines to start the new test
|
|
1287
|
+
* @param {[str]} obj.middle_lines - 0 or more middle lines of the new test
|
|
1288
|
+
* @param {[str]} obj.final_lines - 0 or more lines to end the new test
|
|
1289
|
+
* @param {[str]} obj.warnings - 0 or more warning messages
|
|
1290
|
+
*
|
|
1291
|
+
* @returns {str} - Block of text of the new test
|
|
1292
|
+
* */
|
|
1293
|
+
let one_new_test_text = [
|
|
1294
|
+
...starting_lines, ...middle_lines, ...final_lines,
|
|
1295
|
+
].join(`\n`);
|
|
1296
|
+
|
|
1297
|
+
let edited_for_warnings = edit_for_warning_tag({
|
|
1298
|
+
new_text: one_new_test_text,
|
|
1299
|
+
has_warnings: warnings.length > 0
|
|
1300
|
+
});
|
|
1301
|
+
|
|
1302
|
+
return edited_for_warnings;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
|
|
1306
|
+
function edit_for_warning_tag({ new_text, has_warnings }) {
|
|
1307
|
+
/** Paste in a warning tag if needed, otherwise remove the placeholder text.
|
|
1308
|
+
*
|
|
1309
|
+
* @param {object} obj - Named arguments
|
|
1310
|
+
* @param {str} obj.new_text - The text to edit
|
|
1311
|
+
* @param {boolean} obj.has_warnings - Whether this Scenario has warning
|
|
1312
|
+
* messages
|
|
1313
|
+
*
|
|
1314
|
+
* @returns {str} - The edited text
|
|
1315
|
+
* */
|
|
1316
|
+
if ( has_warnings ) {
|
|
1317
|
+
new_text = add_warning_tag({ new_text });
|
|
1318
|
+
} else {
|
|
1319
|
+
new_text = remove_warning_tag_placeholder({ new_text });
|
|
1320
|
+
}
|
|
1321
|
+
return new_text;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
|
|
1325
|
+
const ALKILN_WARNING_TAG = `@alkiln_generator_warning`;
|
|
1326
|
+
function add_warning_tag({ new_text }) {
|
|
1327
|
+
/** Paste in the warning tag wherever the placeholders are
|
|
1328
|
+
*
|
|
1329
|
+
* @param {object} obj - Named arguments
|
|
1330
|
+
* @param {str} obj.new_text - The text to edit
|
|
1331
|
+
*
|
|
1332
|
+
* @returns {str} - Text with the warning tag added
|
|
1333
|
+
* */
|
|
1334
|
+
return new_text.replace( WARNING_TAG_PLACEHOLDER, ` ${ ALKILN_WARNING_TAG }` );
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
|
|
1338
|
+
function remove_warning_tag_placeholder({ new_text }) {
|
|
1339
|
+
/** Remove the text of the warning tag placeholder to keep things purty.
|
|
1340
|
+
*
|
|
1341
|
+
* @param {object} obj - Named arguments
|
|
1342
|
+
* @param {str} obj.new_text - The text to clean up
|
|
1343
|
+
*
|
|
1344
|
+
* @returns {str} - The cleaned text
|
|
1345
|
+
* */
|
|
1346
|
+
return new_text.replace( WARNING_TAG_PLACEHOLDER, `` );
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
|
|
1350
|
+
function add_total_tests({ scenario_group_str, num_tests }) {
|
|
1351
|
+
/** Paste in the total number of tests generated for this generator Scenario.
|
|
1352
|
+
*
|
|
1353
|
+
* @param {object} obj - Named arguments
|
|
1354
|
+
* @param {str} obj.scenario_group_str - The combined string of all the
|
|
1355
|
+
* Scenarios for this particular generator
|
|
1356
|
+
* @param {[str]} obj.num_tests - Number of tests that ALKiln created
|
|
1357
|
+
*
|
|
1358
|
+
* @returns {str} - The updated string
|
|
1359
|
+
* */
|
|
1360
|
+
let with_num_total_tests = scenario_group_str.replaceAll(
|
|
1361
|
+
TOTAL_TESTS_REGEX, num_tests
|
|
1362
|
+
);
|
|
1363
|
+
return with_num_total_tests;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
|
|
1367
|
+
function add_warnings_Scenario({ scenarios_str, warnings }) {
|
|
1368
|
+
/** Add the given warnings as a new Scenario before all the generated
|
|
1369
|
+
* Scenarios. Basically, a whole-generator-scope warning.
|
|
1370
|
+
*
|
|
1371
|
+
* Discuss: Add a pre-group Scenario for every group of generated Scenarios
|
|
1372
|
+
* identifying the generator Scenario of the subsequent new Scenarios.
|
|
1373
|
+
* That is, even when there is no warning to give.
|
|
1374
|
+
*
|
|
1375
|
+
* TODO: If/when we allow multiple generator Scenarios, use the ID here too
|
|
1376
|
+
*
|
|
1377
|
+
* @param {object} obj - Named arguments
|
|
1378
|
+
* @param {str} obj.scenarios_str - The str of the already existing Scenarios
|
|
1379
|
+
* @param {[str]} obj.warnings - 0 or more warning messages
|
|
1380
|
+
*
|
|
1381
|
+
* @returns {str} - The text of the combined Scenarios.
|
|
1382
|
+
* */
|
|
1383
|
+
let tags = `@alkiln_extra_intro ${ ALKILN_WARNING_TAG }`;
|
|
1384
|
+
let keyword_line = `Scenario: ALKiln humbly begs the author's forgiveness. The next tests might be flawed.`;
|
|
1385
|
+
let step_text = get_goofy_warnings_Step_text();
|
|
1386
|
+
|
|
1387
|
+
let warnings_lines = warnings_as_list_of_lines({ warnings });
|
|
1388
|
+
let warnings_str = get_warnings_docstring({ warnings_lines, indent: 2 });
|
|
1389
|
+
|
|
1390
|
+
let new_Scenario_str = [
|
|
1391
|
+
tags,
|
|
1392
|
+
keyword_line,
|
|
1393
|
+
step_text,
|
|
1394
|
+
warnings_str,
|
|
1395
|
+
].join(`\n`);
|
|
1396
|
+
|
|
1397
|
+
let original_str = scenarios_str;
|
|
1398
|
+
let all_Scenarios_str = [ new_Scenario_str, original_str ].join(`\n\n`);
|
|
1399
|
+
|
|
1400
|
+
// Discuss: Clarity of `join()` expression. Alternatives.
|
|
1401
|
+
// `${new_Scenario_str}\n\n${original_str}`
|
|
1402
|
+
// new_Scenario_str + `\n\n` + original_str
|
|
1403
|
+
// `join()` gets the delimiter out of the way of the items being joined.
|
|
1404
|
+
|
|
1405
|
+
return all_Scenarios_str;
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
|
|
1409
|
+
function get_goofy_warnings_Step_text() {
|
|
1410
|
+
/** Return the text of the "warnings" Step. It's only a bit goofy. We need
|
|
1411
|
+
* more goofs in the world. 0 arguments needed.
|
|
1412
|
+
*
|
|
1413
|
+
* Discuss: A global for the goofy strings would help to keep tests aligned.
|
|
1414
|
+
*
|
|
1415
|
+
* @returns {str} - The text of the "warnings" Step.
|
|
1416
|
+
* */
|
|
1417
|
+
let fun_ones = [
|
|
1418
|
+
`hiccup`, `snafu`, `bumble`, `stumble`, `fumble`, `slip`, `goof`, `flub`
|
|
1419
|
+
];
|
|
1420
|
+
let fun_one = fun_ones[ Math.floor( Math.random() * fun_ones.length ) ];
|
|
1421
|
+
let step_text = ` Then ALKiln warns the author about a generator ${ fun_one }:`;
|
|
1422
|
+
return step_text;
|
|
1423
|
+
};
|
|
1424
|
+
|
|
1425
|
+
|
|
1426
|
+
function get_warnings_docstring({ warnings_lines, indent }) {
|
|
1427
|
+
/** Return the docstring for the warnings Step, including delimiters (`"""`)
|
|
1428
|
+
*
|
|
1429
|
+
* @param {object} obj - Named arguments
|
|
1430
|
+
* @param {[str]} obj.warnings - 0 or more strings
|
|
1431
|
+
* @param {int} obj.indent - How indentation levels to start each line. Note:
|
|
1432
|
+
* Each indentation level currently gets 2 spaces
|
|
1433
|
+
*
|
|
1434
|
+
* @returns {str} - docstring for the warnings Step
|
|
1435
|
+
* */
|
|
1436
|
+
let doc_str_lines = [
|
|
1437
|
+
`"""`,
|
|
1438
|
+
`――――― Generator file warnings ―――――`,
|
|
1439
|
+
...warnings_lines,
|
|
1440
|
+
`―――――`,
|
|
1441
|
+
`"""`,
|
|
1442
|
+
];
|
|
1443
|
+
|
|
1444
|
+
let indented_lines = [];
|
|
1445
|
+
for ( let line of doc_str_lines ) {
|
|
1446
|
+
indented_lines.push( ` `.repeat( indent ) + line );
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
return indented_lines.join(`\n`);
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
|
|
1453
|
+
function warnings_as_list_of_lines({ warnings }) {
|
|
1454
|
+
/** Flatten the list of single- or multi-line warnings into single lines.
|
|
1455
|
+
*
|
|
1456
|
+
* @param {object} obj - Named arguments
|
|
1457
|
+
* @param {[str]} obj.warnings - 0 or more single- or multi-line strings
|
|
1458
|
+
*
|
|
1459
|
+
* @returns {[str]} - 0 or more single-line strings
|
|
1460
|
+
* */
|
|
1461
|
+
let all_lines = [];
|
|
1462
|
+
for ( let warning of warnings ) {
|
|
1463
|
+
all_lines.push( ...warning.split(`\n`) );
|
|
1464
|
+
}
|
|
1465
|
+
return all_lines;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
|
|
1469
|
+
function validate_syntax({ file_text }) {
|
|
1470
|
+
/** Return any errors we get from running the text through the Gherkin parser
|
|
1471
|
+
*
|
|
1472
|
+
* @param {object} obj - Named arguments
|
|
1473
|
+
* @param {str} obj.file_text - Text of the Gherkin file
|
|
1474
|
+
*
|
|
1475
|
+
* @returns {object} rtn - Named return values
|
|
1476
|
+
* @returns {[Error|str]} rtn.errors - 0 or more strings or Errors
|
|
1477
|
+
* */
|
|
1478
|
+
let { errors: generated_parsing_errors } = get_Gherkin_AST({ file_text });
|
|
1479
|
+
return { errors: generated_parsing_errors || [] };
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
|
|
1483
|
+
function save_files({ tests_path, artifacts_path, file_strings }) {
|
|
1484
|
+
/** Save each test file string to an artifacts folder and a folder that
|
|
1485
|
+
* cucumber will use to run tests.
|
|
1486
|
+
*
|
|
1487
|
+
* @param {object} obj - Named arguments
|
|
1488
|
+
* @param {str} tests_path - Path where cucumberjs will find these files
|
|
1489
|
+
* @param {str} artifacts_path - Artifacts folder path
|
|
1490
|
+
* @param {[str]} file_strings - Each string will be a test file
|
|
1491
|
+
*
|
|
1492
|
+
* @returns {undefined}
|
|
1493
|
+
* */
|
|
1494
|
+
// Delete the old files no matter what
|
|
1495
|
+
delete_previous_tests_from_sources_folders_for_ALKiln_developers({ tests_path });
|
|
1496
|
+
|
|
1497
|
+
if ( file_strings.length === 0 ) { return; }
|
|
1498
|
+
|
|
1499
|
+
// Implementation note: This has no error if the folder already exists
|
|
1500
|
+
fs.mkdirSync( tests_path, { recursive: true });
|
|
1501
|
+
fs.mkdirSync( artifacts_path, { recursive: true });
|
|
1502
|
+
|
|
1503
|
+
log.debug({ code: `ALK0269`, context: `generating files`, },
|
|
1504
|
+
`Saving ${ file_strings.length } newly generated files in "${ tests_path }"`
|
|
1505
|
+
);
|
|
1506
|
+
|
|
1507
|
+
let count = 1;
|
|
1508
|
+
for ( let file_str of file_strings ) {
|
|
1509
|
+
let filename = `${ count }_${ Date.now() }.feature`;
|
|
1510
|
+
count++;
|
|
1511
|
+
// Implementation note: If the files don't exist, this creates them
|
|
1512
|
+
fs.appendFileSync( `${ tests_path }/${ filename }`, file_str );
|
|
1513
|
+
fs.appendFileSync( `${ artifacts_path }/${ filename }`, file_str );
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
log.debug({ code: `ALK0270`, context: `generating files`, },
|
|
1517
|
+
`Saved new generated files:`, fs.readdirSync( tests_path )
|
|
1518
|
+
);
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
|
|
1522
|
+
function delete_previous_tests_from_sources_folders_for_ALKiln_developers({ tests_path }) {
|
|
1523
|
+
/** Delete previously generated files from this folder if they exist because
|
|
1524
|
+
* otherwise those tests will run again too (like for ALKiln devs).
|
|
1525
|
+
*
|
|
1526
|
+
* Note: All the relevant tests that cucumber will run will be on the paths
|
|
1527
|
+
* that come in here, so all *relevant* old files will get deleted, even if
|
|
1528
|
+
* there are previous files hanging around somewhere else.
|
|
1529
|
+
*
|
|
1530
|
+
* Research: How will this folder behave on the Playground? Do we need to
|
|
1531
|
+
* clean up at the end too?
|
|
1532
|
+
*
|
|
1533
|
+
* @param {object} obj - Named arguments
|
|
1534
|
+
* @param {str} tests_path - Path where cucumberjs will find these files
|
|
1535
|
+
*
|
|
1536
|
+
* @returns {undefined}
|
|
1537
|
+
* */
|
|
1538
|
+
if ( !fs.existsSync( tests_path )) { return; }
|
|
1539
|
+
log.debug({ code: `ALK0268`, context: `generating files`, },
|
|
1540
|
+
`Deleting previously generated files:`, fs.readdirSync( tests_path )
|
|
1541
|
+
);
|
|
1542
|
+
fs.rmSync( tests_path, { recursive: true, force: true });
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
|
|
1546
|
+
/**
|
|
1547
|
+
* ====================
|
|
1548
|
+
* Shared functions
|
|
1549
|
+
* _In order of first runtime call appearance_
|
|
1550
|
+
* ====================
|
|
1551
|
+
* */
|
|
1552
|
+
|
|
1553
|
+
function as_index( line_num ) {
|
|
1554
|
+
/** Given a Gherkin line number, return the file-lines-list index for that
|
|
1555
|
+
* line number. This is here mostly as a name that helps clarify what the
|
|
1556
|
+
* code is doing.
|
|
1557
|
+
*
|
|
1558
|
+
* Each Gherkin line number matches a line in the file and therefore an item
|
|
1559
|
+
* in the `generator_lines` list. The Gherkin line numbers are 1-indexed.
|
|
1560
|
+
* That means the location of `{line: 1}` in the AST refers to
|
|
1561
|
+
* `generator_lines[0]`.
|
|
1562
|
+
*
|
|
1563
|
+
* @param {int} line_num - A 1-indexed Gherkin line number
|
|
1564
|
+
*
|
|
1565
|
+
* @returns {int} - The corresponding index in the list of lines in the
|
|
1566
|
+
* generator file
|
|
1567
|
+
* */
|
|
1568
|
+
return line_num - 1;
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
|
|
1572
|
+
class Misbehaviors {
|
|
1573
|
+
/** Accumulates and manages errors and warnings lists. This lets you
|
|
1574
|
+
* accumulate errors for a whole function. A loop in that function, though,
|
|
1575
|
+
* can build up warnings just for that loop, after which you can reset the
|
|
1576
|
+
* warnings list for the next loop. Each loop can add up its own warnings.
|
|
1577
|
+
*
|
|
1578
|
+
* Example:
|
|
1579
|
+
*
|
|
1580
|
+
* function do_important_things() {
|
|
1581
|
+
* let misfits = new Misbehaviors();
|
|
1582
|
+
*
|
|
1583
|
+
* let { result, errors } = func();
|
|
1584
|
+
* misfits.add({ errors });
|
|
1585
|
+
*
|
|
1586
|
+
* for ( let item of generator_lines ) {
|
|
1587
|
+
*
|
|
1588
|
+
* misfits.reset_warnings();
|
|
1589
|
+
*
|
|
1590
|
+
* let { foo, warnings1, errors1 } = func1( item );
|
|
1591
|
+
* misfits.add({ warnings: warnings1, errors: errors1 });
|
|
1592
|
+
*
|
|
1593
|
+
* let { bar, warnings2, errors2 } = func2( item );
|
|
1594
|
+
* misfits.add({ warnings: warnings2, errors: errors2 });
|
|
1595
|
+
*
|
|
1596
|
+
* console.log( misfits.warnings );
|
|
1597
|
+
* // Loop 1: [ "oops", "darn" ]
|
|
1598
|
+
* // Loop 2: [ "shoot", "zoinks" ]
|
|
1599
|
+
* }
|
|
1600
|
+
*
|
|
1601
|
+
* console.log( misfits.errors );
|
|
1602
|
+
* // [ "ahh!", "gorramit", "that's done it" ]
|
|
1603
|
+
* }
|
|
1604
|
+
* */
|
|
1605
|
+
constructor() {
|
|
1606
|
+
this.errors = [];
|
|
1607
|
+
this.warnings = [];
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
reset_warnings() {
|
|
1611
|
+
/** Sets warnings to an empty list. This is useful inside a loop that still
|
|
1612
|
+
* needs to keep
|
|
1613
|
+
*
|
|
1614
|
+
* @returns {undefined}
|
|
1615
|
+
* */
|
|
1616
|
+
this.warnings = [];
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
add({ errors=[], warnings=[], ...return_vals }) {
|
|
1620
|
+
/** Adds errors and/or warnings to their list. Returns the same values.
|
|
1621
|
+
*
|
|
1622
|
+
* @param {obj} obj - Named arguments
|
|
1623
|
+
* @param {[str]} obj.warnings - (Optional) 0 or more warning messages
|
|
1624
|
+
* @param {[Error]} obj.errors - (Optional) 0 or more Errors
|
|
1625
|
+
* @param {Any} obj...return_vals - (Optional) Any other values
|
|
1626
|
+
*
|
|
1627
|
+
* @returns {any} - The values it was given
|
|
1628
|
+
* */
|
|
1629
|
+
if ( this.errors ) { this.errors.push( ...errors ); }
|
|
1630
|
+
if ( this.warnings ) { this.warnings.push( ...warnings ); }
|
|
1631
|
+
return { errors, warnings, ...return_vals };
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
steal_misbehaviors() {
|
|
1635
|
+
return this.add( ...arguments );
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
|
|
1640
|
+
function scenario_description_for_logs({ scenario }) {
|
|
1641
|
+
/** Combines metadata to return a fuller description of the Scenario.
|
|
1642
|
+
*
|
|
1643
|
+
* @param {object} obj - Named arguments
|
|
1644
|
+
* @param {object} obj.scenario - Gherkin AST `scenario` object
|
|
1645
|
+
*
|
|
1646
|
+
* @returns {string} - Description of the Scenario
|
|
1647
|
+
* */
|
|
1648
|
+
let description = `line ${ scenario.location.line }: "${ scenario.name }"`;
|
|
1649
|
+
if ( scenario.tags.length > 0 ) {
|
|
1650
|
+
let tag_names = scenario.tags.map((tag)=>{ return tag.name; });
|
|
1651
|
+
description += `, ${ tag_names.join(' ') }`;
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
return description;
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
|
|
1658
|
+
function get_keyword_index({ scenario }) {
|
|
1659
|
+
/** Return the generator file index of the Scenario's keyword line.
|
|
1660
|
+
*
|
|
1661
|
+
* @param {object} obj - Named arguments
|
|
1662
|
+
* @param {object} obj.scenario - Gherkin Scenario object
|
|
1663
|
+
*
|
|
1664
|
+
* @returns {int} - Keyword line index
|
|
1665
|
+
* */
|
|
1666
|
+
return as_index( scenario.location.line );
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
|
|
1670
|
+
function get_tweens({
|
|
1671
|
+
end_index_of_prev=0,
|
|
1672
|
+
start_index_of_next=0,
|
|
1673
|
+
generator_lines=[],
|
|
1674
|
+
indexes_to_delete=[]
|
|
1675
|
+
}) {
|
|
1676
|
+
/** Return a list of strings of the lines in between start and end index,
|
|
1677
|
+
* EXCLUSIVE OF BOTH. If `generator_lines` does not have enough lines for
|
|
1678
|
+
* `start_index_of_next`, it will return values until the end of the list.
|
|
1679
|
+
* This is intended to get comments, blank lines, descriptions, and Steps
|
|
1680
|
+
* that the generator will leave alone.
|
|
1681
|
+
*
|
|
1682
|
+
* IMPORTANT: Remember it excludes both the starting line and the final line
|
|
1683
|
+
*
|
|
1684
|
+
* @Example
|
|
1685
|
+
* let tweens = get_tweens({ end_index_of_prev: 3, start_index_of_next: 6,
|
|
1686
|
+
* [
|
|
1687
|
+
* `Feature: I go the store`, ``, `Scenario: I get apples`,
|
|
1688
|
+
* ` Given ALKiln makes 5 random tests`, `And I start the interview at go.yml`,
|
|
1689
|
+
* ` And I set "groceries" to True`, ` Alkiln can get to "end" with:`
|
|
1690
|
+
* ]
|
|
1691
|
+
* });
|
|
1692
|
+
* // console.log( tweens );
|
|
1693
|
+
* // { tweens: [
|
|
1694
|
+
* // `And I start the interview at go.yml`, ` And I set "groceries" to True`,
|
|
1695
|
+
* // ]}
|
|
1696
|
+
*
|
|
1697
|
+
* @param {object} obj - Named arguments
|
|
1698
|
+
* @param {int} obj.end_index_of_prev - The line that ended the previous
|
|
1699
|
+
* significant file lines (like a fore-Step)
|
|
1700
|
+
* @param {int} obj.start_index_of_next - The line that will start the next
|
|
1701
|
+
* significant file line (like a constraints Step)
|
|
1702
|
+
* @param {[Any]} obj.generator_lines - List of generator_lines
|
|
1703
|
+
* @param {[int|undefined]} obj.indexes_to_delete - 0 or more indexes of
|
|
1704
|
+
* generator_lines to delete from the list
|
|
1705
|
+
*
|
|
1706
|
+
* @returns {obj} rtn - Named return values
|
|
1707
|
+
* @returns {[str]} - 0 or more lines in between the given lines (exclusive)
|
|
1708
|
+
* @returns {[str]} rtn.warnings - 0 or more warning messages
|
|
1709
|
+
* @returns {[Error|str]} rtn.errors - 0 or more strings or Errors
|
|
1710
|
+
* */
|
|
1711
|
+
let errors = [];
|
|
1712
|
+
let warnings = [];
|
|
1713
|
+
let tweens = [];
|
|
1714
|
+
try {
|
|
1715
|
+
let start = end_index_of_prev + 1; // exclude previous significant line
|
|
1716
|
+
for ( let line_i = start; line_i < start_index_of_next; line_i++ ) {
|
|
1717
|
+
if ( indexes_to_delete.includes( line_i )) { continue; }
|
|
1718
|
+
tweens.push( generator_lines[ line_i ]);
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
log.debug({ code: `ALK0246`, context: `generating files`, },
|
|
1722
|
+
`Got tween lines. Starting line (excluded):`, generator_lines[ end_index_of_prev ],
|
|
1723
|
+
`\nTweens:`, tweens,
|
|
1724
|
+
`\nEnding line (excluded):`, generator_lines[ start_index_of_next ]
|
|
1725
|
+
);
|
|
1726
|
+
} catch ( tween_error ) {
|
|
1727
|
+
errors.push( new Error( log.error({ code: `ALK0247`, context: `generating files`, },
|
|
1728
|
+
`ALKiln got an error it is unfamiliar with when generating lines in between other lines starting on Gherkin AST line ${ start }. We would love to see your debug.txt`
|
|
1729
|
+
)));
|
|
1730
|
+
errors.push( tween_error );
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
return { tweens, errors, warnings };
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
|
|
1737
|
+
function get_authors_indent({ located }) {
|
|
1738
|
+
/** Return a whitespace string representing the indentation for a given
|
|
1739
|
+
* Gherkin location object.
|
|
1740
|
+
*
|
|
1741
|
+
* @param {object} obj - Named arguments
|
|
1742
|
+
* @param {object} obj.located - Gherkin AST object with a location property
|
|
1743
|
+
*
|
|
1744
|
+
* @returns {string} - A whitespace string representing the indentation for a
|
|
1745
|
+
* given Gherkin location object.
|
|
1746
|
+
* */
|
|
1747
|
+
if ( typeof located !== `object` || !located.location ) {
|
|
1748
|
+
log.debug({ code: `ALK0253`, context: `generating files`, }
|
|
1749
|
+
`Skipped get_authors_indent. Given argument:\n`,
|
|
1750
|
+
located
|
|
1751
|
+
);
|
|
1752
|
+
return ``;
|
|
1753
|
+
}
|
|
1754
|
+
return ` `.repeat( located.location.column - 1 );
|
|
1755
|
+
}
|