@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.
- package/.github/workflows/github_server.yml +7 -7
- package/.github/workflows/playground.yml +2 -1
- package/CHANGELOG.md +32 -1
- package/action.yml +89 -29
- package/action_for_github_server/action.yml +73 -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 +41 -24
- package/lib/docassemble/takedown.js +33 -10
- package/lib/globals.js +10 -0
- package/lib/run_cucumber.js +155 -50
- package/lib/scope.js +101 -77
- package/lib/setup/TestGenerator.js +1745 -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 +176 -51
- package/lib/utils/Log.js +1132 -0
- package/lib/utils/_alkiln-constrained_random_tests_old.js +1727 -0
- package/lib/utils/files.js +18 -11
- package/lib/utils/get_error_stacks.js +16 -0
- package/lib/utils/reports.js +95 -28
- package/lib/utils/session_vars.js +102 -73
- package/lib/utils/set_sources_paths.js +98 -0
- package/lib/utils/time.js +44 -0
- package/package.json +16 -8
- package/tests/log_codes/check_codes.sh +19 -14
- package/tests/log_codes/log_code_expected_instances.txt +69 -9
- package/tests/unit_tests/constrained_random_answers/invalid.fixtures.js +155 -0
- package/tests/unit_tests/constrained_random_answers/invalid.test.js +233 -0
- package/tests/unit_tests/constrained_random_answers/valid.fixtures.js +905 -0
- package/tests/unit_tests/constrained_random_answers/valid.test.js +521 -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,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
|
|
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
|
-
|
|
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
|
-
//
|
|
99
|
-
//
|
|
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
|
-
//
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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.
|
|
108
|
-
scope.paths.
|
|
109
|
-
scope.paths.
|
|
110
|
-
|
|
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
|
-
//
|
|
118
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
372
|
-
+ `too long to try to reach
|
|
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
|
|
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
|
-
|
|
401
|
-
|
|
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 = `
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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({
|
|
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
|
-
|
|
1302
|
-
|
|
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({
|
|
1401
|
-
|
|
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
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
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.
|
|
1486
|
-
|
|
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.
|
|
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.
|
|
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({
|
|
1643
|
+
log.debug({ code: `ALK0100`, context: `scenario`, },
|
|
1644
|
+
`Scenario After() message:`, scenario.result.message
|
|
1645
|
+
);
|
|
1521
1646
|
|
|
1522
1647
|
});
|
|
1523
1648
|
|