@suffolklitlab/alkiln 5.15.0-feat-random-9 → 5.15.1-fix-race-condition
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 +1 -1
- package/CHANGELOG.md +2 -15
- package/lib/run_cucumber.js +2 -2
- package/lib/scope.js +59 -19
- package/lib/setup/TestGenerator.js +33 -47
- package/lib/setup/artifacts.js +4 -10
- package/lib/setup/validate.js +9 -22
- package/lib/steps.js +0 -16
- package/lib/utils/Log.js +0 -1
- package/lib/utils/files.js +7 -11
- package/lib/utils/session_vars.js +1 -11
- package/lib/utils/set_sources_paths.js +6 -8
- package/npm-shrinkwrap.json +285 -126
- package/package.json +3 -5
- package/tests/unit_tests/constrained_random_answers/valid.fixtures.js +8 -15
- package/tests/unit_tests/constrained_random_answers/valid.test.js +3 -38
- package/docs/decisions/2025_06_12-breaking_news_feat.md +0 -118
- package/docs/version_formatting_rules.md +0 -83
- package/lib/breaking_news.js +0 -1320
- package/tests/unit_tests/breaking_news/breaking_news.test.js +0 -58
|
@@ -160,7 +160,7 @@ jobs:
|
|
|
160
160
|
#### Developer note: You can probably leave the rest out
|
|
161
161
|
#### To learn more, see https://assemblyline.suffolklitlab.org/docs/alkiln/writing/#optional-inputs
|
|
162
162
|
ALKILN_TAG_EXPRESSION: "${{ env.ALKILN_TAG_EXPRESSION }}"
|
|
163
|
-
|
|
163
|
+
ALKILN_VERSION: "@race"
|
|
164
164
|
|
|
165
165
|
#### Developer note: Example of making an issue when tests fail
|
|
166
166
|
#### that includes the text of the failure output file
|
package/CHANGELOG.md
CHANGED
|
@@ -18,49 +18,36 @@ Security - in case of vulnerabilities.
|
|
|
18
18
|
Format:
|
|
19
19
|
|
|
20
20
|
## [Unreleased]
|
|
21
|
-
|
|
22
21
|
-
|
|
23
22
|
|
|
24
23
|
## [1.0.0] - 2021-01-16
|
|
25
24
|
### Added
|
|
26
|
-
|
|
27
25
|
-
|
|
28
26
|
|
|
29
27
|
### Changed
|
|
30
|
-
|
|
31
28
|
-
|
|
32
29
|
|
|
33
30
|
### Deprecated
|
|
34
|
-
|
|
35
31
|
-
|
|
36
32
|
|
|
37
33
|
### Removed
|
|
38
|
-
|
|
39
34
|
-
|
|
40
35
|
|
|
41
36
|
### Fixed
|
|
42
|
-
|
|
43
37
|
-
|
|
44
38
|
|
|
45
39
|
### Security
|
|
46
|
-
|
|
47
40
|
-
|
|
48
41
|
|
|
49
42
|
### Internal
|
|
50
|
-
|
|
51
43
|
-
|
|
52
44
|
-->
|
|
53
45
|
|
|
54
46
|
## [Unreleased]
|
|
55
47
|
|
|
56
|
-
###
|
|
57
|
-
|
|
58
|
-
- "Breaking news" feature for ALKilnInThePlayground - a way new ALKiln versions can talk directly with authors even if ALKilnInThePlayground's version stays the same. ALKilnInThePlayground will be able to get text from ALKiln that ALKilnInThePlayground can turn into Mako and then insert on a screen the author can see.
|
|
59
|
-
|
|
60
|
-
### Changed
|
|
61
|
-
|
|
62
|
-
- Implement constrained random answers Step for ALKilnInThePlayground.
|
|
48
|
+
### Fixed
|
|
63
49
|
|
|
50
|
+
- Fixed race condition between navigating and extracting button text.
|
|
64
51
|
|
|
65
52
|
## [5.15.1] - 2025-08-05
|
|
66
53
|
|
package/lib/run_cucumber.js
CHANGED
|
@@ -111,8 +111,8 @@ async function main() {
|
|
|
111
111
|
*/
|
|
112
112
|
|
|
113
113
|
if ( success ) {
|
|
114
|
-
log.
|
|
115
|
-
`
|
|
114
|
+
log.success({ code: `ALK0054`, context: `post_run`, },
|
|
115
|
+
`Tests succeeded!`
|
|
116
116
|
);
|
|
117
117
|
} else {
|
|
118
118
|
// Don't want to throw error here, so don't pass error object
|
package/lib/scope.js
CHANGED
|
@@ -7,6 +7,7 @@ const { AxePuppeteer } = require('@axe-core/puppeteer');
|
|
|
7
7
|
const safe_filename = require("sanitize-filename");
|
|
8
8
|
// "en" is faster to import: https://github.com/faker-js/faker/issues/1114#issuecomment-1169532948
|
|
9
9
|
const faker = require('@faker-js/faker/locale/en').faker;
|
|
10
|
+
const { v4: uuidv4 } = require('uuid');
|
|
10
11
|
|
|
11
12
|
// Ours
|
|
12
13
|
const pdf_reader = require('./utils/pdf_reader');
|
|
@@ -254,23 +255,21 @@ module.exports = {
|
|
|
254
255
|
if ( options.waitForTimeout ) { await time.waitForTimeout( options.waitForTimeout ); }
|
|
255
256
|
}, // Ends afterStep
|
|
256
257
|
|
|
257
|
-
tapElementAndNavigate: async function(scope, { elem, waitForTimeout=
|
|
258
|
-
/** Tap a given element, detect navigation, and do appropriate related things.
|
|
259
|
-
* TODO: not quite sure if we'll be using `waitForTimeout`. */
|
|
258
|
+
tapElementAndNavigate: async function(scope, { elem, waitForTimeout=null }) {
|
|
259
|
+
/** Tap a given element, detect navigation, and do appropriate related things. */
|
|
260
260
|
|
|
261
261
|
let start_url = await scope.page.url(); // so we can later check for navigation
|
|
262
262
|
|
|
263
263
|
await scope.tapElementAndStayOnPage(scope, {
|
|
264
264
|
elem,
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
elem.evaluate( (el) => { return el.click(); })
|
|
268
|
-
]
|
|
265
|
+
promise_returners: [
|
|
266
|
+
() => { return scope.nav_race(scope, {}).result; },
|
|
267
|
+
() => { return elem.evaluate( (el) => { return el.click(); }) },
|
|
268
|
+
]
|
|
269
269
|
});
|
|
270
270
|
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
// Maybe url change + trigger var change? Maybe just trigger var?
|
|
271
|
+
// For other ideas for detecting navigation, see
|
|
272
|
+
// https://github.com/SuffolkLITLab/ALKiln/issues/229
|
|
274
273
|
let end_url = await scope.page.url();
|
|
275
274
|
let navigated = start_url !== end_url;
|
|
276
275
|
|
|
@@ -282,36 +281,77 @@ module.exports = {
|
|
|
282
281
|
let { question_has_id, id, id_matches } = await scope.examinePageID( scope, 'none to match' );
|
|
283
282
|
|
|
284
283
|
// If navigation, wait for 200ms to let things finish loading and moving
|
|
285
|
-
await scope.afterStep(scope, {
|
|
284
|
+
await scope.afterStep(scope, {
|
|
285
|
+
waitForTimeout: ( waitForTimeout || 200 ),
|
|
286
|
+
navigated: true
|
|
287
|
+
});
|
|
286
288
|
} else {
|
|
287
|
-
|
|
289
|
+
// Discuss: Fail here or later since the test didn't manage to navigate?
|
|
290
|
+
await scope.afterStep(scope, {
|
|
291
|
+
waitForTimeout: ( waitForTimeout || 0 ),
|
|
292
|
+
navigated: false
|
|
293
|
+
});
|
|
288
294
|
}
|
|
289
295
|
}, // Ends scope.tapElementAndNavigate()
|
|
290
296
|
|
|
291
|
-
|
|
297
|
+
nav_race: function(scope, opts={}) {
|
|
298
|
+
/**
|
|
299
|
+
* Return a Promise for a race that detects which, if any, navigation result
|
|
300
|
+
* a navigation had.
|
|
301
|
+
*
|
|
302
|
+
* Warning: Unable to effectively add event listener for `daPageLoad`. Gets
|
|
303
|
+
* overwritten on every page load.
|
|
304
|
+
*
|
|
305
|
+
* @returns
|
|
306
|
+
* { obj }
|
|
307
|
+
* ok { bool } - True if no unexpected errors
|
|
308
|
+
* messages {[ str ]} - Any error or info messages
|
|
309
|
+
* errors {[ Error ]} - Any Errors that happened
|
|
310
|
+
* result { Promise } with result of { obj } - HTTP response or other data
|
|
311
|
+
* aborters {[ obj ]} - Abort signals for all the Promises involved
|
|
312
|
+
* */
|
|
313
|
+
let ok, messages, errors;
|
|
314
|
+
let aborters = [];
|
|
315
|
+
|
|
316
|
+
let result = Promise.race([
|
|
317
|
+
scope.page.waitForNavigation({ waitUntil: 'domcontentloaded' }),
|
|
318
|
+
]);
|
|
319
|
+
|
|
320
|
+
return { result, aborters, ok, messages, errors, };
|
|
321
|
+
}, // Ends scope.nav_race()
|
|
322
|
+
|
|
323
|
+
tapElementAndStayOnPage: async function(scope, { elem, promise_returners=null }) {
|
|
292
324
|
/** Tap a given element, detect navigation, and do appropriate related things.
|
|
293
|
-
|
|
325
|
+
*
|
|
326
|
+
* @param elem { obj } - element to tap
|
|
327
|
+
* @param promise_returners {[ func -> Promise ]} - list of functions that
|
|
328
|
+
* return promises
|
|
294
329
|
*/
|
|
295
330
|
await scope.guard_against_missing_tap_element(scope, { elem });
|
|
296
331
|
|
|
297
332
|
// Get the text on the button or link for a potential error message later
|
|
298
333
|
let tapperText = await (await elem.getProperty('textContent')).jsonValue();
|
|
299
334
|
|
|
300
|
-
|
|
335
|
+
let click_promises = null;
|
|
336
|
+
if ( promise_returners ) {
|
|
337
|
+
click_promises = Promise.all(promise_returners.map( pr_ret => pr_ret() ));
|
|
338
|
+
} else {
|
|
339
|
+
click_promises = elem.click();
|
|
340
|
+
}
|
|
301
341
|
|
|
302
342
|
// TODO: implement and use scope.handle_possible_timeout()
|
|
303
343
|
try {
|
|
304
|
-
//
|
|
344
|
+
// Server error loads page, so keeping things shorter by not looking
|
|
305
345
|
// for that text too. Also, puppeteer will ensure proper timeout.
|
|
306
346
|
await Promise.race([
|
|
307
|
-
|
|
347
|
+
click_promises,
|
|
308
348
|
// This is meant to detect user-visible alerts/errors,
|
|
309
349
|
// not breaking errors in the testing code.
|
|
350
|
+
// No need for aborters since puppeteer has its own timeout
|
|
310
351
|
scope.page.waitForSelector('.alert-danger', { visible: true }),
|
|
311
352
|
scope.page.waitForSelector('.da-has-error', { visible: true }),
|
|
312
353
|
]);
|
|
313
354
|
} catch ( error ) {
|
|
314
|
-
|
|
315
355
|
if ( error.name === `TimeoutError` ) {
|
|
316
356
|
let non_reload_report_msg = `After ALKiln tapped "${ tapperText }" it took too long to load the next page.`;
|
|
317
357
|
await scope.handle_page_timeout_error( scope, { non_reload_report_data: {
|
|
@@ -2729,7 +2769,7 @@ module.exports = {
|
|
|
2729
2769
|
pic_name = `pic_on-${ short_id }-${ timestamp }`;
|
|
2730
2770
|
}
|
|
2731
2771
|
|
|
2732
|
-
await scope.take_a_screenshot( scope, { path:
|
|
2772
|
+
await scope.take_a_screenshot( scope, { path: `./${ scope.paths.scenario }/${ pic_name }.jpg` });
|
|
2733
2773
|
|
|
2734
2774
|
await scope.afterStep( scope );
|
|
2735
2775
|
}, // Ends scope.steps.screenshot()
|
|
@@ -15,13 +15,11 @@ const get_error_stacks = require(`../utils/get_error_stacks`);
|
|
|
15
15
|
* file incorrectly.
|
|
16
16
|
* Discuss: Require author to use "Template Scenario:" as they keyword (Also
|
|
17
17
|
* "Template Scenario Outline:") (or "Scenario Template", maybe harder to
|
|
18
|
-
* scan) for 2 reasons. 1. Gets the vocab of "
|
|
18
|
+
* scan) for 2 reasons. 1. Gets the vocab of "template" in early (though
|
|
19
19
|
* that doesn't match "constraints" Step, which is a whole other language
|
|
20
|
-
* problem we haven't solved yet.) 2. Saying
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* "ALKILN_MAX_RANDOM**IZED**_TESTS_PER_SCENARIO" might be more accurate. Is
|
|
24
|
-
* "ALKILN_MAX_TESTS_PER_GENERATOR" better?
|
|
20
|
+
* problem we haven't solved yet.) 2. Saying "ALKILN_MAX_RANDOM_TESTS_PER_SCENARIO"
|
|
21
|
+
* is confusing when "Scenario" means different things - a template
|
|
22
|
+
* Scenario and a regular Scenario.
|
|
25
23
|
* Discuss: Now that Log is less fragile and duplicated, shall we create a new
|
|
26
24
|
* Log instance in here?
|
|
27
25
|
* Discuss: consider debug types that are for authors. Sort of like warnings,
|
|
@@ -103,13 +101,13 @@ module.exports = class TestGenerator {
|
|
|
103
101
|
}
|
|
104
102
|
}
|
|
105
103
|
|
|
106
|
-
log.debug({ code: `ALK0267`, context: `generating files`, }, `Constrained random answers tests
|
|
104
|
+
log.debug({ code: `ALK0267`, context: `generating files`, }, `Constrained random answers tests:`, new_files_strings );
|
|
107
105
|
|
|
108
106
|
let tests_path = `${ sources_path }/${ FOLDER_NAME }`;
|
|
109
107
|
let artifacts_path = `${ session_vars.get_artifacts_path_name() }/${ FOLDER_NAME }`;
|
|
110
108
|
save_files({ tests_path, artifacts_path, file_strings: new_files_strings });
|
|
111
109
|
|
|
112
|
-
log.
|
|
110
|
+
log.info({ code: `ALK0271`, context: `generating files`, }, `Tried to save generated files to ${ artifacts_path }` );
|
|
113
111
|
|
|
114
112
|
return { new_strings: new_files_strings, errors: all_errors };
|
|
115
113
|
}
|
|
@@ -181,7 +179,7 @@ module.exports = class TestGenerator {
|
|
|
181
179
|
let scenario_objs = doc_AST.feature.children;
|
|
182
180
|
if ( !scenario_objs[0] ) {
|
|
183
181
|
let syntax_error = log.error({ code: `ALK0235`, context: `generating files`, before: `\n\n`, },
|
|
184
|
-
`It looks like the author's
|
|
182
|
+
`It looks like the author's template .feature file at "${ generator_path }" has 0 Scenarios.`
|
|
185
183
|
);
|
|
186
184
|
return { new_contents: null, errors: [new Error( syntax_error )]};
|
|
187
185
|
}
|
|
@@ -189,7 +187,7 @@ module.exports = class TestGenerator {
|
|
|
189
187
|
|
|
190
188
|
if ( scenario_objs.length > 1 ) {
|
|
191
189
|
let too_many_scenarios = log.error({ code: `ALK0236`, context: `generating files`, },
|
|
192
|
-
`A
|
|
190
|
+
`A template file can only have 1 template Scenario. This one has ${ scenario_objs.length }.`
|
|
193
191
|
);
|
|
194
192
|
return { new_contents: ``, errors: [new Error( too_many_scenarios )] };
|
|
195
193
|
}
|
|
@@ -200,14 +198,14 @@ module.exports = class TestGenerator {
|
|
|
200
198
|
|
|
201
199
|
let feature_text = get_Feature_text({ feature: doc_AST.feature, generator_lines });
|
|
202
200
|
|
|
203
|
-
|
|
204
|
-
let { scenarios_strings, total_Scenarios_made, errors: scenarios_errors } = this.get_new_Scenarios_strs({
|
|
201
|
+
let { scenarios_strings, errors: scenarios_errors } = this.get_new_Scenarios_strs({
|
|
205
202
|
scenario_objs,
|
|
206
203
|
generator_lines
|
|
207
204
|
});
|
|
208
205
|
generation_errors.push( ...scenarios_errors );
|
|
209
206
|
|
|
210
|
-
|
|
207
|
+
|
|
208
|
+
let count_msg = `ALKiln generated ${ scenarios_strings.length } Scenario or Scenarios`;
|
|
211
209
|
|
|
212
210
|
if ( scenarios_strings.length === 0 ) {
|
|
213
211
|
let error = log.error({ code: `ALK0263`, context: `generating files`, }, count_msg );
|
|
@@ -233,7 +231,7 @@ module.exports = class TestGenerator {
|
|
|
233
231
|
|
|
234
232
|
get_new_Scenarios_strs({ scenario_objs, generator_lines }) {
|
|
235
233
|
/** Return all the strings of the generated Scenarios and any Errors we ran
|
|
236
|
-
* into.
|
|
234
|
+
* into.
|
|
237
235
|
*
|
|
238
236
|
* @param {object} obj - Named arguments
|
|
239
237
|
* @param {object} obj.scenario_objs - 0 or more Gherkin Scenario objects
|
|
@@ -254,8 +252,7 @@ module.exports = class TestGenerator {
|
|
|
254
252
|
let errs_n_warns = new Misbehaviors();
|
|
255
253
|
|
|
256
254
|
let strs_for_all_new_Scenarios = [];
|
|
257
|
-
|
|
258
|
-
// Soon we want to be able to handle multiple generator Scenarios per file
|
|
255
|
+
// Soon we want to be able to handle multiple template Scenarios per file
|
|
259
256
|
for ( let scenario_obj_i = 0; scenario_obj_i < scenario_objs.length; scenario_obj_i++ ) {
|
|
260
257
|
|
|
261
258
|
errs_n_warns.reset_warnings();
|
|
@@ -275,7 +272,7 @@ module.exports = class TestGenerator {
|
|
|
275
272
|
);
|
|
276
273
|
if ( errs_n_warns.errors.length > 0 ) { continue; }
|
|
277
274
|
|
|
278
|
-
let { new_Scenarios_text
|
|
275
|
+
let { new_Scenarios_text } = errs_n_warns.add(
|
|
279
276
|
this.get_1_text_block_of_all_new_Scenarios_from_1_generator_Scenario({
|
|
280
277
|
scenario: generator_Scenario,
|
|
281
278
|
generator_lines: generator_lines,
|
|
@@ -284,7 +281,6 @@ module.exports = class TestGenerator {
|
|
|
284
281
|
})
|
|
285
282
|
);
|
|
286
283
|
if ( errs_n_warns.errors.length > 0 ) { continue; }
|
|
287
|
-
total_Scenarios_made += num_made;
|
|
288
284
|
|
|
289
285
|
if ( errs_n_warns.warnings.length > 0 ) {
|
|
290
286
|
new_Scenarios_text = add_warnings_Scenario({
|
|
@@ -296,7 +292,7 @@ module.exports = class TestGenerator {
|
|
|
296
292
|
strs_for_all_new_Scenarios.push( new_Scenarios_text );
|
|
297
293
|
} // ends for every scenario_container
|
|
298
294
|
|
|
299
|
-
return { scenarios_strings: strs_for_all_new_Scenarios,
|
|
295
|
+
return { scenarios_strings: strs_for_all_new_Scenarios, ...errs_n_warns };
|
|
300
296
|
}
|
|
301
297
|
|
|
302
298
|
|
|
@@ -376,32 +372,22 @@ module.exports = class TestGenerator {
|
|
|
376
372
|
|
|
377
373
|
let got_fewer_tests_than_desired = new_test_bodies.length < num_to_make;
|
|
378
374
|
if ( got_fewer_tests_than_desired ) {
|
|
379
|
-
let
|
|
380
|
-
|
|
381
|
-
+ ` ${ num_to_make } the author wanted for this Scenario. This probably`
|
|
382
|
-
+ ` happened because ALKiln makes sure every test is unique^1. Maybe`
|
|
383
|
-
+ ` there were too few combinations for ALKiln to use. The Scenario:`
|
|
384
|
-
+ `\n ${ scenario_description_for_logs({ scenario }) }`
|
|
385
|
-
+ `\n\n[^1]: "Unique tests" are only unique from the other tests ALKiln`
|
|
386
|
-
+ ` is generating *right now*. ALKiln doesn't check your regular tests.`;
|
|
387
|
-
|
|
388
|
-
let warn_too_few = log.warn({ code: `ALK0260`, context: `generating files generator-scenario-level`, },
|
|
389
|
-
fewer_test_msg,
|
|
375
|
+
let warn_too_few = log.warn({ code: `ALK0260`, context: `generating files template-scenario-level`, },
|
|
376
|
+
`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 from a previous template. The Scenario:\n ${ scenario_description_for_logs({ scenario }) }`,
|
|
390
377
|
)
|
|
391
378
|
// Discuss: Should we add more structure to Misbehaviors to give them
|
|
392
|
-
// context levels (Feature,
|
|
379
|
+
// context levels (Feature, template Scenario, generated Scenario)
|
|
393
380
|
misfits.add({ warnings: [ warn_too_few ] });
|
|
394
381
|
}
|
|
395
382
|
|
|
396
383
|
let new_Scenarios_text = new_test_bodies.join(`\n\n`);
|
|
397
|
-
let num_made = new_test_bodies.length;
|
|
398
384
|
new_Scenarios_text = add_total_tests({
|
|
399
|
-
scenario_group_str: new_Scenarios_text, num_tests:
|
|
385
|
+
scenario_group_str: new_Scenarios_text, num_tests: new_test_bodies.length
|
|
400
386
|
});
|
|
401
387
|
|
|
402
388
|
log.debug({ code: `ALK0261`, context: `generating files`, }, `new_Scenarios_text before adding warnings:\n`, new_Scenarios_text );
|
|
403
389
|
|
|
404
|
-
return { new_Scenarios_text,
|
|
390
|
+
return { new_Scenarios_text, ...misfits };
|
|
405
391
|
}
|
|
406
392
|
|
|
407
393
|
try_to_get_one_unique_middle({
|
|
@@ -509,7 +495,7 @@ function get_generator_AST({ file_text, generator_path }) {
|
|
|
509
495
|
|
|
510
496
|
if ( generator_errors.length > 0 ) {
|
|
511
497
|
let syntax_error = log.error({ code: `ALK0232`, context: `generating files`, before: `\n\n`, },
|
|
512
|
-
`The author's
|
|
498
|
+
`The author's template .feature file at "${ generator_path }" has Gherkin syntax error(s). You might see more warnings above or below this one.\n`,
|
|
513
499
|
...get_error_stacks({ errors: generator_errors })
|
|
514
500
|
);
|
|
515
501
|
return { doc_AST: null, errors: [new Error( syntax_error )] };
|
|
@@ -519,14 +505,14 @@ function get_generator_AST({ file_text, generator_path }) {
|
|
|
519
505
|
|
|
520
506
|
if ( !doc_AST.feature ) {
|
|
521
507
|
let feature_error = log.error({ code: `ALK0233`, context: `generating files`, before: `\n\n`, },
|
|
522
|
-
`The author's
|
|
508
|
+
`The author's template .feature file at "${ generator_path }" needs to include the \`Feature\` keyword.`
|
|
523
509
|
);
|
|
524
510
|
return { doc_AST: null, errors: [new Error( feature_error )] };
|
|
525
511
|
}
|
|
526
512
|
|
|
527
513
|
// Should not get here
|
|
528
514
|
let unknown_error = log.error({ code: `ALK0234`, context: `generating files`, before: `\n\n`, },
|
|
529
|
-
`The author's
|
|
515
|
+
`The author's template .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.`
|
|
530
516
|
);
|
|
531
517
|
return { doc_AST, errors: [new Error( unknown_error )] };
|
|
532
518
|
}
|
|
@@ -673,7 +659,7 @@ function get_fore_Steps({ scenario }) {
|
|
|
673
659
|
if ( there_were_no_fore_Steps ) {
|
|
674
660
|
misfits.add({ errors: [
|
|
675
661
|
new Error( log.error({ code: `ALK0242`, context: `generating files`, },
|
|
676
|
-
`A
|
|
662
|
+
`A template Scenario's first Step must say how many constrained random answers tests to make. The template Scenario on line ${ scenario.location.line } is missing that Step. The Scenario's description is:\n${ description }`
|
|
677
663
|
) )
|
|
678
664
|
] });
|
|
679
665
|
} else {
|
|
@@ -740,7 +726,7 @@ function get_number_of_Scenarios_to_generate2({ step, prev_num_to_make }) {
|
|
|
740
726
|
// a way that is also maintainable?
|
|
741
727
|
warnings.push(
|
|
742
728
|
log.warn({ code: `ALK0238`, context: `generating files`, },
|
|
743
|
-
`A
|
|
729
|
+
`A template 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 }`
|
|
744
730
|
)
|
|
745
731
|
);
|
|
746
732
|
}
|
|
@@ -751,7 +737,7 @@ function get_number_of_Scenarios_to_generate2({ step, prev_num_to_make }) {
|
|
|
751
737
|
if ( number_matcher === null ) {
|
|
752
738
|
warnings.push(
|
|
753
739
|
log.warn({ code: `ALK0239`, context: `generating files`, },
|
|
754
|
-
`A
|
|
740
|
+
`A template Scenario is missing the number of tests to make. ALKiln will now make 1 constrained random answers test. The current Step:\n ${ text }`
|
|
755
741
|
)
|
|
756
742
|
);
|
|
757
743
|
return { num_to_make: 1, warnings, errors };
|
|
@@ -1115,7 +1101,7 @@ function get_final_ids({ step_text }) {
|
|
|
1115
1101
|
let ids = step_text.match( CAPTURE_QUESTION_IDS_REGEX );
|
|
1116
1102
|
|
|
1117
1103
|
if ( ids === null ) {
|
|
1118
|
-
let missing_ids_msg = `The constrained random answers Step is missing its question ids. ALKiln is unable to create random tests for this
|
|
1104
|
+
let missing_ids_msg = `The constrained random answers Step is missing its question ids. ALKiln is unable to create random tests for this template Scenario. The Step:\n${ step_text }`;
|
|
1119
1105
|
let error = log.error({ code: `ALK0250`, context: `generating files`, }, missing_ids_msg );
|
|
1120
1106
|
errors.push(new Error( error ));
|
|
1121
1107
|
}
|
|
@@ -1364,7 +1350,7 @@ function remove_warning_tag_placeholder({ new_text }) {
|
|
|
1364
1350
|
|
|
1365
1351
|
|
|
1366
1352
|
function add_total_tests({ scenario_group_str, num_tests }) {
|
|
1367
|
-
/** Paste in the total number of tests generated for this
|
|
1353
|
+
/** Paste in the total number of tests generated for this template Scenario.
|
|
1368
1354
|
*
|
|
1369
1355
|
* @param {object} obj - Named arguments
|
|
1370
1356
|
* @param {str} obj.scenario_group_str - The combined string of all the
|
|
@@ -1385,10 +1371,10 @@ function add_warnings_Scenario({ scenarios_str, warnings }) {
|
|
|
1385
1371
|
* Scenarios. Basically, a whole-generator-scope warning.
|
|
1386
1372
|
*
|
|
1387
1373
|
* Discuss: Add a pre-group Scenario for every group of generated Scenarios
|
|
1388
|
-
* identifying the
|
|
1374
|
+
* identifying the template Scenario of the subsequent new Scenarios.
|
|
1389
1375
|
* That is, even when there is no warning to give.
|
|
1390
1376
|
*
|
|
1391
|
-
* TODO: If/when we allow multiple
|
|
1377
|
+
* TODO: If/when we allow multiple template Scenarios, use the ID here too
|
|
1392
1378
|
*
|
|
1393
1379
|
* @param {object} obj - Named arguments
|
|
1394
1380
|
* @param {str} obj.scenarios_str - The str of the already existing Scenarios
|
|
@@ -1517,7 +1503,7 @@ function save_files({ tests_path, artifacts_path, file_strings }) {
|
|
|
1517
1503
|
fs.mkdirSync( artifacts_path, { recursive: true });
|
|
1518
1504
|
|
|
1519
1505
|
log.debug({ code: `ALK0269`, context: `generating files`, },
|
|
1520
|
-
`Saving ${ file_strings.length } newly generated files in "${ tests_path }"
|
|
1506
|
+
`Saving ${ file_strings.length } newly generated files in "${ tests_path }"`
|
|
1521
1507
|
);
|
|
1522
1508
|
|
|
1523
1509
|
let count = 1;
|
|
@@ -1529,8 +1515,8 @@ function save_files({ tests_path, artifacts_path, file_strings }) {
|
|
|
1529
1515
|
fs.appendFileSync( `${ artifacts_path }/${ filename }`, file_str );
|
|
1530
1516
|
}
|
|
1531
1517
|
|
|
1532
|
-
log.
|
|
1533
|
-
`Saved new generated files
|
|
1518
|
+
log.debug({ code: `ALK0270`, context: `generating files`, },
|
|
1519
|
+
`Saved new generated files:`, fs.readdirSync( tests_path )
|
|
1534
1520
|
);
|
|
1535
1521
|
}
|
|
1536
1522
|
|
package/lib/setup/artifacts.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const session_vars = require(`../utils/session_vars`);
|
|
4
4
|
const files = require(`../utils/files`);
|
|
5
|
-
const Log = require(`../
|
|
5
|
+
const Log = require(`../setup/artifacts.js`);
|
|
6
6
|
|
|
7
7
|
/** Reset and refresh artifacts path name */
|
|
8
8
|
|
|
@@ -12,15 +12,9 @@ session_vars.delete_artifacts_path_name();
|
|
|
12
12
|
// If using `npm run cucumber` (as opposed to the bin commands),
|
|
13
13
|
// use `--` before `--sources=./foo`
|
|
14
14
|
const argv = require(`minimist`)( process.argv.slice(2) );
|
|
15
|
-
|
|
16
|
-
const artifacts_path = files.make_artifacts_folder();
|
|
15
|
+
const artifacts_path = files.make_artifacts_folder( argv.path );
|
|
17
16
|
|
|
18
17
|
session_vars.save_artifacts_path_name( artifacts_path );
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
`Created the artifacts folder at "${ artifacts_path }"`
|
|
23
|
-
);
|
|
24
|
-
|
|
25
|
-
// This is just for artifacts, not any other config values. Leave the config
|
|
26
|
-
// Project name value as it is. Local developers find it useful to re-use it.
|
|
19
|
+
// Leave the config Project name value as it is. Local developers find it
|
|
20
|
+
// useful to re-use that value.
|
package/lib/setup/validate.js
CHANGED
|
@@ -32,6 +32,7 @@ function validate_Gherkin_files() {
|
|
|
32
32
|
* generator files. If there are any, it logs those errors and then finally
|
|
33
33
|
* throws an error. Uses the values in command line arguments.
|
|
34
34
|
* */
|
|
35
|
+
|
|
35
36
|
let sources_paths = set_sources_paths({ log });
|
|
36
37
|
|
|
37
38
|
let all_errors = [];
|
|
@@ -43,16 +44,10 @@ function validate_Gherkin_files() {
|
|
|
43
44
|
// Discuss: If an author has only some files with syntax errors, should we try
|
|
44
45
|
// to still run the tests for the files that had valid syntax?
|
|
45
46
|
if ( all_errors.length > 0 ) {
|
|
46
|
-
let parse_error_msg = `ALKiln ran into Gherkin syntax error(s) when parsing `
|
|
47
|
-
+ `these test feature files. See warnings and errors above.`
|
|
48
47
|
log.throw({ code: `ALK0230`, context: `validating files`,
|
|
49
|
-
error: new Error(
|
|
48
|
+
error: new Error( `ALKiln ran into Gherkin syntax error(s) when parsing your test feature files. See warnings and errors above.` )
|
|
50
49
|
});
|
|
51
50
|
};
|
|
52
|
-
|
|
53
|
-
log.info({ code: `ALK0283`, context: `validating files`,},
|
|
54
|
-
`Validated all feature files`
|
|
55
|
-
);
|
|
56
51
|
}
|
|
57
52
|
|
|
58
53
|
|
|
@@ -69,23 +64,15 @@ function parse_all_feature_files_in({ sources_path }) {
|
|
|
69
64
|
* */
|
|
70
65
|
let all_AST_errors = [];
|
|
71
66
|
|
|
72
|
-
let feature_file_paths =
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
{ ignore: [`${ sources_path }/${ globals.generated_files_folder_name }/*`] }
|
|
78
|
-
);
|
|
79
|
-
} catch ( feature_paths_error ) {
|
|
80
|
-
log.error({ code: `ALK0282`, context: `validating files`, },
|
|
81
|
-
`Error while trying to find feature files at "${ sources_path }"`,
|
|
82
|
-
feature_paths_error
|
|
83
|
-
);
|
|
84
|
-
return { errors: [ feature_paths_error ]};
|
|
85
|
-
}
|
|
67
|
+
let feature_file_paths = fast_glob.sync(
|
|
68
|
+
`${ sources_path }/**/*.feature`,
|
|
69
|
+
// These files were generated last time and will get deleted before the run
|
|
70
|
+
{ ignore: [`${ sources_path }/${ globals.generated_files_folder_name }/*`] }
|
|
71
|
+
);
|
|
86
72
|
|
|
87
73
|
for ( let feature_path of feature_file_paths ) {
|
|
88
|
-
|
|
74
|
+
|
|
75
|
+
let file_text = fs.readFileSync( feature_path, { encoding: `utf8` });
|
|
89
76
|
let { AST, errors: these_AST_errors } = get_Gherkin_AST({ file_text });
|
|
90
77
|
|
|
91
78
|
if ( these_AST_errors.length > 0 ) {
|
package/lib/steps.js
CHANGED
|
@@ -25,9 +25,6 @@ const session_vars = require('./utils/session_vars');
|
|
|
25
25
|
const files = require('./utils/files' );
|
|
26
26
|
const reports = require(`./utils/reports`);
|
|
27
27
|
const da_api = require('./docassemble/docassemble_api_interface');
|
|
28
|
-
const globals = require(`./globals`);
|
|
29
|
-
|
|
30
|
-
const GENERATED_FILES_FOLDER_NAME = globals.generated_files_folder_name;
|
|
31
28
|
|
|
32
29
|
/* Of Note:
|
|
33
30
|
- We're using `*=` for selectors because sometimes da text has funny characters in it that are hard to anticipate
|
|
@@ -121,9 +118,6 @@ Before(async (scenario) => {
|
|
|
121
118
|
|
|
122
119
|
// Start the running "progress bar" for the Scenario
|
|
123
120
|
log.stdout({}, `\nScenario: ${ scenario.pickle.name }: `);
|
|
124
|
-
// Save Scenario data in Scenario folder
|
|
125
|
-
// '@alkiln @generated'
|
|
126
|
-
// console.log( JSON.stringify( scenario ) );
|
|
127
121
|
|
|
128
122
|
// Create browser, which can't be created in BeforeAll() where .driver doesn't exist for some reason
|
|
129
123
|
if (!scope.browser) {
|
|
@@ -161,16 +155,6 @@ Before(async (scenario) => {
|
|
|
161
155
|
fs.mkdirSync( scope.paths.scenario );
|
|
162
156
|
// Store path name for this Scenario's individualized report
|
|
163
157
|
scope.paths.scenario_report = `${ scope.paths.scenario }/report.txt`;
|
|
164
|
-
// If it's a generated Scenario, save the `.feature` file in here
|
|
165
|
-
// GENERATED_FILES_FOLDER_NAME
|
|
166
|
-
if ( scenario.gherkinDocument.uri.includes("_alkiln_generated") ) {
|
|
167
|
-
try {
|
|
168
|
-
let file_text = fs.readFileSync( scenario.gherkinDocument.uri, {encoding: `utf8`});
|
|
169
|
-
fs.appendFileSync( `${ scope.paths.scenario }/${ filename }`, file_text );
|
|
170
|
-
} catch ( saving_feat_file_error ) {
|
|
171
|
-
// Ignore. This is not crucial.
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
158
|
|
|
175
159
|
// Downloads
|
|
176
160
|
scope.downloadComplete = false;
|
package/lib/utils/Log.js
CHANGED
package/lib/utils/files.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const safe_filename = require('sanitize-filename');
|
|
3
|
-
const path = require(`path`);
|
|
4
3
|
|
|
5
4
|
let files = {};
|
|
6
5
|
|
|
@@ -22,30 +21,27 @@ files.readable_date = function() {
|
|
|
22
21
|
return fancy_str;
|
|
23
22
|
}; // Ends files.readable_date()
|
|
24
23
|
|
|
25
|
-
files.make_artifacts_folder = function (
|
|
24
|
+
files.make_artifacts_folder = function (path) {
|
|
26
25
|
/** Makes a folder in the root of the project if it doesn't
|
|
27
26
|
* already exist, either with the given name or with a default name.
|
|
28
27
|
*
|
|
29
28
|
* Works for local development as well, without action.yml or workflow files.
|
|
30
29
|
* */
|
|
31
|
-
if (
|
|
32
|
-
let name_parts =
|
|
30
|
+
if (path) {
|
|
31
|
+
let name_parts = path.split(`/`);
|
|
33
32
|
let safe_parts = [];
|
|
34
33
|
for ( let part of name_parts ) {
|
|
35
34
|
safe_parts.push(safe_filename( part ));
|
|
36
35
|
}
|
|
37
|
-
|
|
36
|
+
path = safe_parts.join(`/`);
|
|
38
37
|
} else {
|
|
39
|
-
|
|
38
|
+
path = `alkiln-${ files.readable_date() }`
|
|
40
39
|
}
|
|
41
40
|
|
|
42
|
-
const root = process.cwd();
|
|
43
|
-
root_path = path.join( root, file_path );
|
|
44
|
-
|
|
45
41
|
// `recursive` avoids errors if folder already exists
|
|
46
42
|
// https://nodejs.org/docs/latest-v18.x/api/fs.html#fspromisesmkdirpath-options
|
|
47
|
-
fs.mkdirSync(
|
|
48
|
-
return
|
|
43
|
+
fs.mkdirSync( path, { recursive: true });
|
|
44
|
+
return path;
|
|
49
45
|
}; // Ends files.make_artifacts_folder()
|
|
50
46
|
|
|
51
47
|
module.exports = files;
|