@suffolklitlab/alkiln 5.13.2 → 5.13.3-feat-random

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.
Files changed (49) hide show
  1. package/.github/workflows/github_server.yml +7 -7
  2. package/.github/workflows/playground.yml +2 -1
  3. package/CHANGELOG.md +32 -1
  4. package/action.yml +89 -29
  5. package/action_for_github_server/action.yml +73 -29
  6. package/docs/4_environments.md +243 -0
  7. package/docs/artifacts_folder_name.md +135 -0
  8. package/docs/constrained_random_tests.md +254 -0
  9. package/docs/decisions/006_which_log_level_to_pick_2024_09_5.md +70 -0
  10. package/docs/decisions/007_getting_and_storing_logs.md +179 -0
  11. package/docs/decisions/move_runtime_config_to_sources_folder.md +5 -0
  12. package/docs/decisions/reports_and_logs_2024_08_11.md +161 -0
  13. package/docs/kinds_of_errors.md +26 -0
  14. package/docs/prioritize_constrained_random_tests.md +60 -0
  15. package/lib/docassemble/docassemble_api_interface.js +77 -51
  16. package/lib/docassemble/server_install.js +32 -9
  17. package/lib/docassemble/setup.js +41 -24
  18. package/lib/docassemble/takedown.js +33 -10
  19. package/lib/globals.js +10 -0
  20. package/lib/run_cucumber.js +155 -50
  21. package/lib/scope.js +101 -77
  22. package/lib/setup/TestGenerator.js +1745 -0
  23. package/lib/setup/artifacts.js +20 -0
  24. package/lib/setup/generate.js +42 -0
  25. package/lib/setup/get_Gherkin_AST.js +26 -0
  26. package/lib/setup/validate.js +89 -0
  27. package/lib/steps.js +176 -51
  28. package/lib/utils/Log.js +1132 -0
  29. package/lib/utils/_alkiln-constrained_random_tests_old.js +1727 -0
  30. package/lib/utils/files.js +18 -11
  31. package/lib/utils/get_error_stacks.js +16 -0
  32. package/lib/utils/reports.js +95 -28
  33. package/lib/utils/session_vars.js +102 -73
  34. package/lib/utils/set_sources_paths.js +98 -0
  35. package/lib/utils/time.js +44 -0
  36. package/package.json +16 -8
  37. package/tests/log_codes/check_codes.sh +19 -14
  38. package/tests/log_codes/log_code_expected_instances.txt +69 -9
  39. package/tests/unit_tests/constrained_random_answers/invalid.fixtures.js +155 -0
  40. package/tests/unit_tests/constrained_random_answers/invalid.test.js +233 -0
  41. package/tests/unit_tests/constrained_random_answers/valid.fixtures.js +905 -0
  42. package/tests/unit_tests/constrained_random_answers/valid.test.js +521 -0
  43. package/tests/unit_tests/example_of_chai_test_lifecycle.js +78 -0
  44. package/tests/unit_tests/getAllFields.test.js +5 -0
  45. package/tests/unit_tests/getMatchingRows.test.js +5 -0
  46. package/tests/unit_tests/log.test.js +557 -0
  47. package/tests/unit_tests/normalizeTable.test.js +5 -0
  48. package/lib/utils/log.js +0 -96
  49. /package/docs/{expected_inernal_results.md → expected_internal_results.md} +0 -0
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+
3
+ const session_vars = require(`../utils/session_vars`);
4
+ const files = require(`../utils/files`);
5
+ const Log = require(`../setup/artifacts.js`);
6
+
7
+ /** Reset and refresh artifacts path name */
8
+
9
+ session_vars.delete_artifacts_path_name();
10
+
11
+ // process.argv is a list of strings of commands and args.
12
+ // If using `npm run cucumber` (as opposed to the bin commands),
13
+ // use `--` before `--sources=./foo`
14
+ const argv = require(`minimist`)( process.argv.slice(2) );
15
+ const artifacts_path = files.make_artifacts_folder( argv.path );
16
+
17
+ session_vars.save_artifacts_path_name( artifacts_path );
18
+
19
+ // Leave the config Project name value as it is. Local developers find it
20
+ // useful to re-use that value.
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env node
2
+
3
+ const Log = require(`../utils/Log`);
4
+ const set_sources_paths = require(`../utils/set_sources_paths`);
5
+ const TestGenerator = require(`./TestGenerator.js`);
6
+
7
+ // process.argv is a list of strings of commands and args.
8
+ // If using `npm run cucumber` (as opposed to the bin commands),
9
+ // use `--` before `--sources=./foo`
10
+ const argv = require(`minimist`)( process.argv.slice(2) );
11
+ const log = new Log({ path: argv.path, context: `generating files` });
12
+ // Run script
13
+ generate_constrained_random_test_files();
14
+
15
+
16
+ function generate_constrained_random_test_files() {
17
+ /** Executable that generates constrained random answers tests and throws
18
+ * errors.
19
+ *
20
+ * Takes no arguments - uses command line arguments instead.
21
+ * */
22
+ let all_errors = [];
23
+
24
+ // Every file that's executed separately needs to figure out the location of
25
+ // the sources folders.
26
+ let sources_paths = set_sources_paths({ log });
27
+ let generator = new TestGenerator();
28
+ for ( let sources_path of sources_paths ) {
29
+ let { errors: generating_errors } = generator.create({
30
+ sources_path,
31
+ logger: log
32
+ });
33
+ all_errors.push( ...generating_errors );
34
+ }
35
+
36
+ if ( all_errors.length > 0 ) {
37
+ log.throw({ code: `ALK0270`, context: `generating files`,
38
+ error: new Error( `ALKiln got error(s) when it tried to generate constrained random answers tests. See warnings and errors above.` )
39
+ });
40
+ };
41
+
42
+ };
@@ -0,0 +1,26 @@
1
+ const Gherkin = require(`@cucumber/gherkin`);
2
+ const Messages = require(`@cucumber/messages`);
3
+
4
+ module.exports = function get_Gherkin_AST({ file_text }) {
5
+ /** Return Gherkin parser's abstract syntax tree of the given generator file
6
+ *
7
+ * @param {object} obj - Named arguments
8
+ * @param {string} file_text - The generator file's text contents
9
+ *
10
+ * @returns {object} - Gherkin parser's AST of the given generator file
11
+ * */
12
+ let AST = null;
13
+ let parse_errors = [];
14
+ try {
15
+ let uuidFn = Messages.IdGenerator.uuid();
16
+ let builder = new Gherkin.AstBuilder(uuidFn);
17
+ let matcher = new Gherkin.GherkinClassicTokenMatcher(); // or Gherkin.GherkinInMarkdownTokenMatcher()
18
+
19
+ let parser = new Gherkin.Parser(builder, matcher);
20
+ AST = parser.parse( file_text );
21
+ } catch ( gherkin_error ) {
22
+ parse_errors.push( gherkin_error );
23
+ }
24
+
25
+ return { AST, errors: parse_errors };
26
+ }
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require(`fs`);
4
+ const fast_glob = require(`fast-glob`);
5
+
6
+ const globals = require(`../globals`);
7
+ const Log = require(`../utils/Log`);
8
+ const set_sources_paths = require(`../utils/set_sources_paths`);
9
+ const get_Gherkin_AST = require(`./get_Gherkin_AST`);
10
+ const get_error_stacks = require(`../utils/get_error_stacks`);
11
+
12
+
13
+ /**
14
+ * TODO: Document that generator files need to have valid Gherkin syntax
15
+ * TODO: Document how to read Gherkin syntax errors. E.g.
16
+ * (3:1): expected: #EOF, #Language, #TagLine, #FeatureLine, #Comment, #Empty, got 'Scenario: bar'
17
+ * (6:1): expected: #EOF, #TableRow, #DocStringSeparator, #StepLine, #TagLine, #ExamplesLine, #ScenarioLine, #RuleLine, #Comment, #Empty, got 'Feature: zoo'
18
+ * */
19
+
20
+
21
+ // process.argv is a list of strings of commands and args.
22
+ // If using `npm run cucumber` (as opposed to the bin commands),
23
+ // use `--` before `--sources=./foo`
24
+ const argv = require(`minimist`)( process.argv.slice(2) );
25
+ const log = new Log({ path: argv.path, context: `validating files` });
26
+ // Run script
27
+ validate_Gherkin_files();
28
+
29
+
30
+ function validate_Gherkin_files() {
31
+ /** Accumulates syntax errors for all `.feature`` test files, including
32
+ * generator files. If there are any, it logs those errors and then finally
33
+ * throws an error. Uses the values in command line arguments.
34
+ * */
35
+
36
+ let sources_paths = set_sources_paths({ log });
37
+
38
+ let all_errors = [];
39
+ for ( let sources_path of sources_paths ) {
40
+ let { errors: files_errors } = parse_all_feature_files_in({ sources_path });
41
+ all_errors.push( ...files_errors );
42
+ }
43
+
44
+ // Discuss: If an author has only some files with syntax errors, should we try
45
+ // to still run the tests for the files that had valid syntax?
46
+ if ( all_errors.length > 0 ) {
47
+ log.throw({ code: `ALK0230`, context: `validating files`,
48
+ error: new Error( `ALKiln ran into Gherkin syntax error(s) when parsing your test feature files. See warnings and errors above.` )
49
+ });
50
+ };
51
+ }
52
+
53
+
54
+ function parse_all_feature_files_in({ sources_path }) {
55
+ /** Returns accumulated list of Gherkin errors from all files in the
56
+ * docassemble "sources" folder (or an empty list).
57
+ *
58
+ * @param {object} obj - Named arguments
59
+ * @param {object} obj.sources_path - Path to the docassemble "sources" folder
60
+ *
61
+ * @returns {{errors: [Error]}} Obj with an `error` prop that is a list of
62
+ * Error instances. Follows the format of other functions that return
63
+ * accumulated errors.
64
+ * */
65
+ let all_AST_errors = [];
66
+
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
+ );
72
+
73
+ for ( let feature_path of feature_file_paths ) {
74
+
75
+ let file_text = fs.readFileSync( feature_path, { encoding: `utf8` });
76
+ let { AST, errors: these_AST_errors } = get_Gherkin_AST({ file_text });
77
+
78
+ if ( these_AST_errors.length > 0 ) {
79
+ all_AST_errors.push( ...these_AST_errors );
80
+ log.warn({ code: `ALK0274`, context: `validating files`, },
81
+ `There is a Gherkin syntax error(s) in "${ feature_path }"`,
82
+ ...get_error_stacks({ errors: these_AST_errors })
83
+ );
84
+ }
85
+
86
+ } // ends for each file path
87
+
88
+ return { errors: all_AST_errors };
89
+ }
package/lib/steps.js CHANGED
@@ -5,6 +5,8 @@ const {
5
5
  Given,
6
6
  After,
7
7
  Before,
8
+ BeforeStep,
9
+ AfterStep,
8
10
  BeforeAll,
9
11
  AfterAll,
10
12
  setDefaultTimeout //,
@@ -18,7 +20,7 @@ const { v4: uuidv4 } = require('uuid');
18
20
 
19
21
  // Ours
20
22
  const scope = require('./scope');
21
- const log = require('./utils/log');
23
+ const Log = require(`./utils/Log`);
22
24
  const session_vars = require('./utils/session_vars');
23
25
  const files = require('./utils/files' );
24
26
  const reports = require(`./utils/reports`);
@@ -82,7 +84,9 @@ let click_with = {
82
84
  pc: 'click',
83
85
  };
84
86
 
85
- BeforeAll(async () => {
87
+ let log = null;
88
+
89
+ BeforeAll(async function() {
86
90
  scope.showif_timeout = 600;
87
91
  // TODO: Do these really need to be maps? As sets, they may make the output
88
92
  // more predictable to the user, but they add complexity.
@@ -95,29 +99,30 @@ BeforeAll(async () => {
95
99
  // Store path names for files and docs created for the tests
96
100
  scope.paths = {};
97
101
 
98
- // TODO: figure out how to test file and folder creation stuff,
99
- // especially when setup is excluded from the flow.
102
+ // // Make the folder to store all the files for this run of the tests
103
+ // scope.paths.artifacts = files.make_artifacts_folder();
104
+ // // Save its name in a file. `session_vars` can get it back later.
105
+ // // The value we are saving is used by at least run_cucumber.js.
100
106
 
101
- // Make the folder to store all the files for this run of the tests
102
- scope.paths.artifacts = files.make_artifacts_folder();
103
- // Save its name in a file. `session_vars` can get it back later.
104
- // The value we are saving is used by at least run_cucumber.js.
105
- session_vars.save_artifacts_path_name( scope.paths.artifacts );
107
+ // A little indirect, but the artifacts path should always have already been
108
+ // created by now, either by `run_cucumber.js` or the relevant test
109
+ scope.paths.artifacts = session_vars.get_artifacts_path_name();
110
+ log = new Log({ path: scope.paths.artifacts, context: `steps` });
111
+ scope.set_log({ log_obj: log });
106
112
 
107
- scope.paths.report = `${ scope.paths.artifacts }/report.txt`;
108
- scope.paths.failures = `${ scope.paths.artifacts }/${ log.unexpected_filename }`;
109
- scope.paths.debug_log = `${ scope.paths.artifacts }/${ log.debug_log_file }`;
110
- fs.writeFileSync( scope.paths.debug_log, "" );
113
+ scope.paths.debug_log = `${ scope.paths.artifacts }/${ log.debug_log_filename }`;
114
+ scope.paths.report_log = `${ scope.paths.artifacts }/${ log.report_log_filename }`;
115
+ scope.paths.report = `${ scope.paths.artifacts }/${ log.report_filename }`;
116
+ scope.paths.debug_of_unexpected = `${ scope.paths.artifacts }/${ log.temp_unexpected_debug_log_filename }`;
117
+ scope.paths.report_of_unexpected = `${ scope.paths.artifacts }/${ log.unexpected_filename }`;
111
118
 
112
119
  reports.create( scope );
113
120
  });
114
121
 
115
122
  Before(async (scenario) => {
116
123
 
117
- // Useful for those with a lot of tests
118
- // Is unable to print the first Scenario name for some reason
119
- console.log('');
120
- process.stdout.write(`Scenario: ${ scenario.pickle.name }: `);
124
+ // Start the running "progress bar" for the Scenario
125
+ log.stdout({}, `\nScenario: ${ scenario.pickle.name }: `);
121
126
 
122
127
  // Create browser, which can't be created in BeforeAll() where .driver doesn't exist for some reason
123
128
  if (!scope.browser) {
@@ -175,6 +180,14 @@ Before(async (scenario) => {
175
180
 
176
181
  // Add a check for an error page before each step? After each step?
177
182
 
183
+ BeforeStep(async function ( step ) {
184
+ // TODO: Add a way for the reports to add "✔" or "✖" to the start of a Step
185
+ // after it's been completed, as cucumber already does. Add a 'step' type
186
+ // and additional success/failure information, though that may be
187
+ // challenging
188
+ reports.addToReport(scope, { type: `row`, value: step.pickleStep.text });
189
+ reports.indent();
190
+ });
178
191
 
179
192
  //#####################################
180
193
  //#####################################
@@ -342,14 +355,24 @@ Given(/the max(?:imum)? sec(?:ond)?s for each step (?:in this scenario )?is (\d+
342
355
  // TODO: Consider more permissive sentences:
343
356
  // with(?: this data| these answers)?:?
344
357
  // with(?:.*):?
345
- When(/^(?:the user gets)?(?:I get)? to ?(?:the)?(?: ?question)?(?: id)? "([^"]+)" with this data:?/, {timeout: -1}, async (target_id, raw_var_data) => {
358
+ // ^(?:the user|I)? gets? to ?.*?(".*") ?with .*?
359
+ // .*gets? to ?.*?(".*") ?with.*?
360
+ // More readable?:
361
+ // I? ?gets? to ?.*?(".*") ?with.*?(?:this data:)?
362
+ // get to + stop at (?:gets? to|stops? at)
363
+ // `before "a", "b", "c" and after` -> `"a", "b", "c"`
364
+ // Impossible with cucumber `before "a", "b", "c" and after` -> `["a", "b", "c"]`
365
+ const INCLUDING_QUOTES_REGEX = /"([^"]*)"/g;
366
+ When(/I? ?gets? to ?.*?(".*").*?with.*?(?:this data:)?/, {timeout: -1}, async (target_ids, raw_var_data) => {
346
367
  /* NO CUCUMBER TIMEOUT. Must handle timeout ourselves:
347
368
  * https://github.com/cucumber/cucumber-js/blob/master/docs/support_files/timeouts.md#disable-timeouts
348
369
  * Iterate in here to keep timeout reasonable */
349
370
 
350
371
  // Note: Why do we not just stop when we've used all the vars in the table or
351
372
  // reach the last var? Vars are not required to be given in sequence and pages that
352
- // have non-button fields don't usually have a var assigned to the 'continue' button.
373
+ // have non-button fields often don't have a var assigned to the 'continue' button.
374
+ // It is optional for the table to use all the vars and some vars may be used
375
+ // multiple times.
353
376
  // Alternative: require vars even for 'continue' pages. Then in loop require at least one var
354
377
  // to be found on a page before hitting 'continue'. That requires detecting navigation because
355
378
  // pages with fields will not have a var for the continue button, so there will
@@ -357,10 +380,18 @@ When(/^(?:the user gets)?(?:I get)? to ?(?:the)?(?: ?question)?(?: id)? "([^"]+)
357
380
  // It also seems harsh, but more reliable.
358
381
  // Ask developers what they'd like.
359
382
 
383
+ let ids_as_list = Array.from(
384
+ target_ids.matchAll( INCLUDING_QUOTES_REGEX ),
385
+ ( match ) => { return match[ 1 ]; }
386
+ );
387
+ let sanitized_ids = ids_as_list.map(( risky_id ) => {
388
+ return scope.to_docassemble_sanitized_id( risky_id );
389
+ });
360
390
 
361
391
  let supported_table = await scope.normalizeTable( scope, { raw_var_data });
362
392
 
363
- let { question_has_id, id, id_matches } = await scope.examinePageID( scope, target_id );
393
+ let id = await scope.get_page_id( scope );
394
+ let id_matches = id && sanitized_ids.includes( id );
364
395
  // Until we reach the target page, hit an error, or time out (take too long)
365
396
  while ( !id_matches ) {
366
397
  let custom_timeout = new Promise(function( resolve, reject ) {
@@ -368,8 +399,8 @@ When(/^(?:the user gets)?(?:I get)? to ?(?:the)?(?: ?question)?(?: id)? "([^"]+)
368
399
  // Set up the timeout that avoids infinite loops
369
400
  let on_page_timeout = async function() {
370
401
 
371
- let non_reload_report_msg = `The target id was "${ target_id }", but it took `
372
- + `too long to try to reach it (over ${ scope.timeout/1000/60 } min). The `
402
+ let non_reload_report_msg = `The target id or ids were ${ target_ids }, but it took `
403
+ + `too long to try to reach a valid id (over ${ scope.timeout/1000/60 } min). The `
373
404
  + `test got stuck on "${ id }".`
374
405
  let error = non_reload_report_msg;
375
406
  await scope.handle_page_timeout_error( scope, { non_reload_report_data: {
@@ -386,7 +417,7 @@ When(/^(?:the user gets)?(?:I get)? to ?(?:the)?(?: ?question)?(?: id)? "([^"]+)
386
417
  // Maybe list of timeout and/or interval ids in state that can all be cleared at once?
387
418
  (async function() {
388
419
  try {
389
- let { question_has_id, id, id_matches } = await scope.examinePageID( scope );
420
+ let id = await scope.get_page_id( scope );
390
421
 
391
422
  // THIS FUNCTION ACTUALLY MATTERS
392
423
  // Add special rows. Basically, do weirder things.
@@ -396,20 +427,19 @@ When(/^(?:the user gets)?(?:I get)? to ?(?:the)?(?: ?question)?(?: id)? "([^"]+)
396
427
  from_story_table: true,
397
428
  });
398
429
 
399
- log.debug({
400
- type: `info`,
401
- code: `ALK0061`,
402
- pre: `Rows, including special rows:, ${JSON.stringify( ensured_var_data )}`,
403
- });
430
+ log.debug({ code: `ALK0061`, context: `table`, },
431
+ `Rows, including special rows:`, ensured_var_data
432
+ );
404
433
 
405
434
  // THIS FUNCTION ACTUALLY MATTERS
406
435
  // Some errors happen in there to be closer to the relevant code. We'd have to try/catch anyway.
407
436
  let error = await scope.setFields(scope, { var_data: ensured_var_data, id, ensure_navigation: true });
408
437
 
438
+ // TODO: use Log class to throw this error?
409
439
  if ( error.was_found ) { await scope.throwPageError( scope, { id: id }); }
410
440
  resolve();
411
441
  } catch ( err ) {
412
- let err_msg = `Error occurred when tried to answer fields on question "${ id }".`
442
+ let err_msg = `An error occurred when ALKiln tried to answer fields on question with the id "${ id }". See above for warnings and errors.`
413
443
  reports.addToReport( scope, { type: `error`, code: `ALK0062`, value: err_msg });
414
444
  // Throw error instead?
415
445
  reject( err );
@@ -422,7 +452,8 @@ When(/^(?:the user gets)?(?:I get)? to ?(?:the)?(?: ?question)?(?: id)? "([^"]+)
422
452
  await custom_timeout;
423
453
 
424
454
  // Prep for next loop
425
- ({ question_has_id, id, id_matches } = await scope.examinePageID( scope, target_id ));
455
+ id = await scope.get_page_id( scope );
456
+ id_matches = id && sanitized_ids.includes( id );
426
457
 
427
458
  } // ends while !id_matches
428
459
  // Can anything unsatisfactory get through here?
@@ -441,7 +472,7 @@ When(/^(?:the user gets)?(?:I get)? to ?(?:the)?(?: ?question)?(?: id)? "([^"]+)
441
472
  // Add full used table to report so it can be copied if needed.
442
473
  // TODO: Should we/How do we get this in the final scenario report instead of here?
443
474
  reports.addToReport( scope, { type: `note info`, code: `ALK0063`, value: `\n${ ' '.repeat(2) }Rows that got set:` });
444
- reports.addToReport( scope, { type: `step info`, code: `ALK0064`, value: `${ ' '.repeat(4) }And I get to the question id "${ target_id }" with this data:` });
475
+ reports.addToReport( scope, { type: `step info`, code: `ALK0064`, value: `${ ' '.repeat(4) }And I get to any of the question ids ${ target_ids } with this data:` });
445
476
  reports.addToReport(scope, { type: `row info`, code: `ALK0065`, value: `${ ' '.repeat(6) }| var | value | trigger |`, });
446
477
  let at_least_one_row_was_NOT_used = false;
447
478
  for ( let row of sorted_rows ) {
@@ -467,6 +498,54 @@ When(/^(?:the user gets)?(?:I get)? to ?(?:the)?(?: ?question)?(?: id)? "([^"]+)
467
498
  reports.addToReport(scope, { type: `plain`, value: `` });
468
499
  });
469
500
 
501
+ /**
502
+ *
503
+ * | var | possible_values |
504
+ * | favorite_color | blue;; green;; red |
505
+ *
506
+ * @param raw_var_data { [{ var, possible_values }] } List of table row objects
507
+ * @param raw_var_data[i].var {str} Name of the variable to set
508
+ * @param raw_var_data[i].possible_values {str} Format is ";;" separated
509
+ * possible values for the variable (lawyers use ";" grammatically)
510
+ * */
511
+
512
+ /** Need a sentence for:
513
+ * And I will go to the interview at the address ("")
514
+ * And I will run (100) constrained random tests
515
+ * And those tests should get to one of these page ids - [("lsdjf", "sldkjf")]
516
+ * And I will get there with random choices from this data:
517
+ * | var | possible_values |
518
+ * | favorite_color | blue;; green;; red |
519
+ *
520
+ * Write files
521
+ * 1. Make an object from the given constraints table (split by single _and_
522
+ * double quotes? Just double quotes? Document author _must_ use double
523
+ * quotes?)
524
+ * 1. Make sure that it's not the same as previous objects
525
+ * 1. Far in the future: Make sure it's not the same as a test the author
526
+ * already wrote (need to wrap cucumber for this to run all random tests
527
+ * last)
528
+ * 1. Make a Scenario description using the iteration number
529
+ * 1. Add to one big file (name includes datetime) for all these Scenarios
530
+ * 1. Increment iteration number
531
+ * 1. Leave them there to get run later
532
+ *
533
+ * Supposedly more performant to write one big file [^citation needed]. Not
534
+ * sure it matters that much yet. If we do want to optimize, maybe using
535
+ * a write stream directly is the way to go.
536
+ *
537
+ * Command:
538
+ * I run (\d*) random tests for/at "interview.yml" that will reach one of these question/page ids (\[".*"]) with these constraints: (table)
539
+ * (\d*) random at "interview.yml" will reach one/any of (\[.*]) with: (table)
540
+ *
541
+ * Given I generate 100 random tests
542
+ * And I test the interview at ""
543
+ * And I reach any of these question/page ids: ""
544
+ * And I answer with/give these possible answers: (table)
545
+ * I generate 100 random tests for the interview at "" to reach any of the question ids [] with these possible answers
546
+ *
547
+ */
548
+
470
549
 
471
550
  //#####################################
472
551
  //#####################################
@@ -972,7 +1051,7 @@ When(/I answer randomly for at most ([0-9]+) (?:page(?:s)?|screen(?:s)?)/, { tim
972
1051
  } // ends if !has_content
973
1052
 
974
1053
  if ( buttons_exist ) {
975
- process.stdout.write(`\x1b[36m${ 'v' }\x1b[0m`); // pressed button
1054
+ log.stdout({}, `\x1b[36m${ 'v' }\x1b[0m`); // pressed button
976
1055
  }
977
1056
 
978
1057
  resolve();
@@ -1150,6 +1229,22 @@ When(/I set the address of ?(?:the var(?:iable)?)? "([^"]+)" to "([^"]+)"/, { ti
1150
1229
  // Next macro - download sequence?
1151
1230
 
1152
1231
 
1232
+ //#####################################
1233
+ //#####################################
1234
+ // Messages from ALKiln to author
1235
+ //#####################################
1236
+ //#####################################
1237
+
1238
+ const LOG_CODE_REGEX = /ALKiln\d\d\d\d/;
1239
+ Given(/ALKiln warns the author/i, async ( warnings_text ) => {
1240
+ /** Create a warning message for the author files they wrote to generate
1241
+ * constrained random answers tests.
1242
+ *
1243
+ * @param {str} warnings_text - Warning text generated by ALKiln
1244
+ * */
1245
+ reports.addToReport( scope, { type: `as_is`, value: warnings_text });
1246
+ })
1247
+
1153
1248
 
1154
1249
  //#####################################
1155
1250
  //#####################################
@@ -1187,6 +1282,8 @@ Given(/the Scenario report should not include/i, async ( report_text ) => {
1187
1282
  Given(/the final Scenario status should be "([^"]+)"/i, async ( expected_status ) => {
1188
1283
  /** WARNING: If a Step will fail, this Step must come first.
1189
1284
  *
1285
+ * TODO: Status "A" ambiguous (internal)
1286
+ *
1190
1287
  * Prepare to test that the Scenario completes with the correct status. */
1191
1288
  scope.expected_status = expected_status.toUpperCase();
1192
1289
  });
@@ -1263,11 +1360,24 @@ Then(/I fail to delete (\d) detected interview(?:s)? and get no error __internal
1263
1360
  //#####################################
1264
1361
  //#####################################
1265
1362
 
1363
+ AfterStep(async function({ result }) {
1364
+ // TODO: Abstract this
1365
+ if ( result.status === `PASSED` ) { log.stdout({ records_only: true }, `.`); }
1366
+ else if ( result.status === `FAILED` ) { log.stdout({ records_only: true }, `F`); }
1367
+ else if ( result.status === `UNDEFINED` ) { log.stdout({ records_only: true }, `U`); }
1368
+ else if ( result.status === `SKIPPED` ) { log.stdout({ records_only: true }, `-`); }
1369
+ else if ( result.status === `PENDING` ) { log.stdout({ records_only: true }, `P`); }
1370
+
1371
+ reports.outdent();
1372
+ });
1373
+
1266
1374
  After(async function(scenario) {
1267
1375
 
1268
1376
  // Log errors
1269
1377
  if ( scenario.result.message ) {
1270
- log.debug({ type: `info`, code: `ALK0091`, pre: `cucumber error: ${ scenario.result.message }` }); //, verbose: true });
1378
+ log.debug({ code: `ALK0091`, context: `scenario`, },
1379
+ scenario.result.message
1380
+ );
1271
1381
  }
1272
1382
 
1273
1383
  let changeable_test_status = scenario.result.status;
@@ -1297,11 +1407,9 @@ After(async function(scenario) {
1297
1407
  reports.saveFinalScenarioData( scope, { status: changeable_test_status });
1298
1408
 
1299
1409
  if (changeable_test_status !== `PASSED`) {
1300
- log.debug({
1301
- type: `error`,
1302
- code: `ALK0094`,
1303
- pre: `Non-passed status occurred while running test: ${changeable_test_status}`,
1304
- });
1410
+ log.debug({ code: `ALK0094`, level: `warn`, },
1411
+ `Non-passed status occurred while running test: ${ changeable_test_status }`
1412
+ );
1305
1413
 
1306
1414
  if ( scope.page ) {
1307
1415
 
@@ -1397,8 +1505,9 @@ After(async function(scenario) {
1397
1505
  + `Expected status: ${scope.expected_status}. `
1398
1506
  + `See the report for details.`;
1399
1507
  let code = `ALK0099`;
1400
- log.debug({ type: `error`, code: code, pre: msg, })
1401
- expected_status_err_msg = `🤕 ${ code } ERROR: ${ msg }`
1508
+ expected_status_err_msg = log.debug({ code: code, level: `error`, },
1509
+ msg
1510
+ );
1402
1511
  }
1403
1512
  }
1404
1513
  scope.expected_status = null; // reset for next Scenario
@@ -1418,17 +1527,25 @@ After(async function(scenario) {
1418
1527
  // Make sure that tests which ALKiln has calculated as failed
1419
1528
  // cause a cucumber error later. Just changing the status to `FAILED`
1420
1529
  // causes unhelpful cucumber errors. TODO: Add to internal docs.
1421
- log.debug({
1422
- type: `error`,
1423
- code: force_failure_code,
1424
- pre: force_failure_msg
1425
- });
1530
+ // TODO: Should we move this to where the error is thrown?
1531
+ log.debug({ code: force_failure_code, level: `error`, },
1532
+ force_failure_msg
1533
+ );
1426
1534
  // TODO: Look into setDefinitionFunctionWrapper to catch errors
1427
1535
  // of failing tests that should indeed be failing:
1428
1536
  // https://github.com/cucumber/cucumber-js/blob/main/docs/support_files/api_reference.md#setdefinitionfunctionwrapperwrapper
1429
1537
  need_to_force_failed_status = true;
1430
1538
  }
1431
1539
 
1540
+ // Not sure if this is necessary
1541
+ if ( changeable_test_status === 'PASSED' ) {
1542
+ log.stdout({ records_only: true }, `.` );
1543
+ } else if ( changeable_test_status === `SKIPPED` ) {
1544
+ log.stdout({ records_only: true }, `-` );
1545
+ } else {
1546
+ log.stdout({ records_only: true }, changeable_test_status[0].toUpperCase() );
1547
+ }
1548
+
1432
1549
  let signout_succeeded = true;
1433
1550
  let signout_error_msg = ``;
1434
1551
  // If there is a page open, then sign out and close it
@@ -1482,9 +1599,10 @@ After(async function(scenario) {
1482
1599
  // going to the wrong file). If it becomes a problem we can track it down later.
1483
1600
  if ( visible_keys.length > 0 ) {
1484
1601
  // Can an API key ever see an interview that it can't delete?
1485
- log.info({ type: `internal warning`, code: `ALK0197`,
1486
- pre: `The API key can see ${ visible_keys.length } interviews that were not deleted: ${ visible_keys.join( ', ' ) }`
1487
- });
1602
+ log.debug({ code: `ALK0197`, context: `internal`, level: `warn`, }
1603
+ `The API key can see ${ visible_keys.length } interviews that were not deleted:`,
1604
+ visible_keys
1605
+ );
1488
1606
  }
1489
1607
  } catch ( error ) {
1490
1608
  // Deleting interviews is nice, but not non-critical. Don't bother the
@@ -1494,7 +1612,9 @@ After(async function(scenario) {
1494
1612
  // interview. If it becomes a problem we can track it down later.
1495
1613
  // Consider add "internal" to docs: "If you see this, make an
1496
1614
  // issue in ALKiln with your debug/verbose log"
1497
- log.info({ type: `internal warning`, code: `ALK0198`, pre: `Error while cleaning up interviews.`, data: error });
1615
+ log.debug({ code: `ALK0198`, context: `internal`, level: `warn`, error },
1616
+ `Cleaning up interviews incomplete, but unimportant.`
1617
+ );
1498
1618
  }
1499
1619
  }
1500
1620
 
@@ -1513,11 +1633,16 @@ After(async function(scenario) {
1513
1633
  throw new Error( `🤕 ${ force_failure_code } unreachable ERROR: ${ force_failure_msg }` );
1514
1634
 
1515
1635
  } else if ( internal_errors.length > 0 ) {
1516
- log.info({ type: `internal error`, code: `ALK0203`, pre: `No need to force failed status and yet there are error messages. ${ errors.join( errors, '\n') }` });
1636
+ log.debug({ code: `ALK0203`, context: `internal`, level: `warn`, },
1637
+ `There was no need to force failed status and yet there are error messages.`,
1638
+ internal_errors
1639
+ );
1517
1640
  }
1518
1641
 
1519
1642
  // Find the race condition.
1520
- log.debug({ type: `cucumber`, code: `ALK0100`, verbose: true, pre: `Scenario AfterStep() message`, data: scenario.result.message });
1643
+ log.debug({ code: `ALK0100`, context: `scenario`, },
1644
+ `Scenario After() message:`, scenario.result.message
1645
+ );
1521
1646
 
1522
1647
  });
1523
1648