@suffolklitlab/alkiln 5.13.4 → 5.15.0-deps-2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/.github/workflows/github_server.yml +1 -1
  2. package/.github/workflows/playground.yml +2 -1
  3. package/.github/workflows/unit_tests.yml +1 -1
  4. package/CHANGELOG.md +40 -0
  5. package/action.yml +26 -10
  6. package/action_for_github_server/action.yml +31 -27
  7. package/docs/4_environments.md +2 -2
  8. package/docs/artifacts_folder_name.md +121 -71
  9. package/docs/constrained_random_tests.md +220 -289
  10. package/docs/decisions/move_runtime_config_to_sources_folder.md +3 -1
  11. package/docs/prioritize_constrained_random_tests.md +35 -76
  12. package/lib/docassemble/server_install.js +0 -2
  13. package/lib/docassemble/setup.js +1 -3
  14. package/lib/docassemble/takedown.js +2 -2
  15. package/lib/globals.js +10 -0
  16. package/lib/run_cucumber.js +40 -53
  17. package/lib/scope.js +44 -1
  18. package/lib/setup/TestGenerator.js +1757 -0
  19. package/lib/setup/artifacts.js +20 -0
  20. package/lib/setup/generate.js +42 -0
  21. package/lib/setup/get_Gherkin_AST.js +26 -0
  22. package/lib/setup/validate.js +89 -0
  23. package/lib/steps.js +101 -42
  24. package/lib/utils/Log.js +317 -165
  25. package/lib/utils/_alkiln-constrained_random_tests_old.js +1776 -0
  26. package/lib/utils/get_error_stacks.js +16 -0
  27. package/lib/utils/pdf_reader.js +9 -2
  28. package/lib/utils/reports.js +44 -22
  29. package/lib/utils/session_vars.js +77 -57
  30. package/lib/utils/set_sources_paths.js +98 -0
  31. package/npm-shrinkwrap.json +739 -924
  32. package/package.json +17 -10
  33. package/tests/log_codes/check_codes.sh +17 -13
  34. package/tests/log_codes/log_code_expected_instances.txt +58 -0
  35. package/tests/unit_tests/constrained_random_answers/invalid.fixtures.js +178 -0
  36. package/tests/unit_tests/constrained_random_answers/invalid.test.js +251 -0
  37. package/tests/unit_tests/constrained_random_answers/valid.fixtures.js +957 -0
  38. package/tests/unit_tests/constrained_random_answers/valid.test.js +563 -0
  39. package/tests/unit_tests/example_of_chai_test_lifecycle.js +78 -0
  40. package/tests/unit_tests/log.test.js +17 -41
  41. package/docs/seprate_file_generation_GitHub_action.md +0 -11
  42. package/lib/parse.js +0 -36
  43. package/lib/utils/get_sources_paths.js +0 -162
  44. package/lib/utils/parse.js +0 -1
  45. package/lib/utils/proccess_command_line_args.js +0 -267
  46. /package/docs/{expected_inernal_results.md → expected_internal_results.md} +0 -0
@@ -1,101 +1,60 @@
1
- # Constrained random input tests
1
+ # Prioritize constrained random answers tests over pure random answers tests
2
2
 
3
- I do 10 constrained random tests
3
+ ## Context and Problem Statement
4
4
 
5
- ## Why do people want purely random input tests?
5
+ We could develop constrained randomness first or pure randomness first. Is there a reason to prioritize one over another?
6
6
 
7
- 1. They don't have time to write individual specific tests
8
- 2. They are worried they haven't covered all eventualities
7
+ ## Considered Options
9
8
 
10
- ## Problems for purely random tests
9
+ - Purely random answers. They fill in fields without any guidance.
10
+ - Constrained random answers tests. They make the test author list all the variable names and all the choices of answers they want to allow for those variables.
11
+ - Hybrid approach. Allow an author to specify some values while letting other values stay completely random.
11
12
 
12
- - Non-specific input. E.g. a name field might get a sentence. Can we do our best? That would be more work.
13
+ ## Decision Outcome
13
14
 
14
- ## Why would people prefer constrained random input tests?
15
+ Prioritize constrained random answers tests over pure random answers tests. Strangely, they seem to be the more flexible of the two options.
15
16
 
16
- - They want to make the tests more focused on certain scenarios and avoid wasting time on unimportant tests. For example, sometimes several options actually give the same answer. A purely random test would give more preference to the repeated answer.
17
- - **Some fields might require very specific answers, like a real lawyer's bar number or a specific file name for uploading exhibits.**
18
- - Narrow down the paths at some points, like always choosing that you have minor children
19
- - **Even if the user doesn't know it, this is the most flexible option**
17
+ ## Pros and Cons of the Options
20
18
 
21
- ## Note
19
+ The discussion about the general pros and cons of random answers tests will be in the `constrained_random_tests` doc for now.
22
20
 
23
- ~~If in the future we want to add the option to not specify, then we can and it won't be a breaking change.~~
21
+ ### Purely random answers
24
22
 
25
- We have to decide what it means to allow a person to not specify. E.g if excluding a row means don't give an answer to that variable at all, then it can't in future mean "give a random answer to this field". Options:
23
+ Pros:
26
24
 
27
- - (bad) Excluding a row means don't give an answer. Including a row with a blank answer means give a random answer.
28
- - (bad) Including a row with a blank answer means don't give an answer. Give unlisted variables random answers.
29
- - Or should we require a string (e.g. ALK_NO_ANSWER) for fields that should not get random inputs?
30
- - (most easy purely random input method to implement without changing current code?) Or specify randomness (e.g. ALK_RANDOM_ANSWER)?
31
- - **(backwards compatible because we can become more flexible later) Require both of the above?**
25
+ - Very little work for test authors to write.
26
+ - Because they're so free-form, they may do a better job finding unexpected behavior that authors didn't think to test for.
32
27
 
33
- Which of these is worse? Mind reading can be bad. On the other hand, specifying everything may be somewhat antithetical to the 2 goals of people who want purely random input tests. (added to the interview an unrequired value, but forgot to add it to the test, they won't test all eventualities).
28
+ Cons:
34
29
 
35
- Possible solutions for people who want purely random tests or bits of tests:
36
- - Add a Step for answering randomly for a whole test.
37
- - Add a Step for answering randomly up to various page ids (list is allowed).
38
- - Add Step to answer randomly for one page.
30
+ - Unless we can find better ways of generating purely random test files for the authors, copying a random answers test to keep permanently might be very challenging.
31
+ - Fields might get nonsensical answers. For example, a very large number for age or a very small number for income.
32
+ - Impossible to use for fields that need very specific answers, like a real lawyer's bar number or a specific file name for uploading exhibits.
39
33
 
40
- ## Flexible constraints?
34
+ ### Constrained random answers
41
35
 
42
- Post MVP
36
+ Pros:
43
37
 
44
- - Allow number range "ALKiln: [1, 5)"? Or they have to do "1, 2, 3, 4"
45
- - "ALKiln: year 1965 - 1999"
46
- - "ALKiln: date [today, today - 10)"
47
- - "ALKiln: max length 3" (of 5 checkbox answers)
48
- - "ALKiln: name"
49
- - "ALKiln: number" (and other types like "currency") (we can probably figure those out)
38
+ - Useful for fields that need very specific answers, like a real lawyer's bar number or a specific file name for uploading exhibits.
39
+ - An author can focus the tests' answers on important user stories. For example, they might want to focus some tests on users that have children while keeping other answers more random.
40
+ - An author might be able to make more efficient random tests. For example, a multiple choice question may have multiple answers that, behind the scenes, set the same value. Authors might do this to help users who are confused about their options give more accurate answers. A purely random test would pick that repeated value too often and waste testing time.
50
41
 
51
- ## Note
42
+ Cons:
52
43
 
53
- MVP of random input tests
44
+ - They take more time to write.
45
+ - They take more work to maintain.
54
46
 
55
- ## no seeds
47
+ <!-- TODO: Add to docs - each `possible_values` cell should contain 1 or more possible answers. -->
56
48
 
57
- seeds can break more easily. instead we save the spec of the test(s)
49
+ ### Hybrid
58
50
 
59
- ## MVP
51
+ In future, we believe we can figure out how to let users have partially constrained tests where unspecified fields get purely random answers while constrained fields choose from a set of answers. It would need to be its own Step.
60
52
 
61
- save the spec of the test...somewhere. Probably using special name to say
62
- "rerun" and then change the name at the end of the run.
63
- retry the test somehow (if needed)...
64
- present the test and results to the user
53
+ Pros:
65
54
 
66
- ## Better
55
+ - Offers the most flexibility and control.
67
56
 
68
- Avoid repeating previous random tests
69
- Avoid repeating tests of users (which is much(?) harder)
57
+ Cons:
70
58
 
71
- ## Specifics
72
-
73
- Create a file with a certain # of Scenarios and run it
74
- - Does cucumber decide the tests it'll run at runtime or before? I think the former
75
- - Is it better to split tests up into separate files if there are a lot?
76
- - Is it better or worse to help the developer feel the magnitude of the #
77
- of tests they're running (using Example) [Answer: worse]
78
- - How to handle separate constrained random tables?
79
-
80
- ## right now
81
- [r1 opt 1, r1 opt 2, r1 opt 3, name input, date input, description input]
82
-
83
- ## need for random
84
-
85
- detect r1 while looping through the fields
86
- [r1 opt 1, r1 opt 2, r1 opt 3, name input, date input, description input]
87
- Con: as we loop through, we don't know the number of options at each particular instance
88
-
89
- or make grouped fields into groups. Basically group r1:
90
- [[r1 opt 1, r1 opt 2, r1 opt 3], name input, date input, description input]
91
-
92
- ## why not generate a test that uses all the constraints and thus is fully random?
93
-
94
- Because we'd have to parse docassembled *compiled* files, which is very hard
95
-
96
- Alternative:
97
-
98
- When the developer runs a test, they can use a Step to have it collect all possible fields that ALKiln sees during that test run.
99
- Q: How effectively can we decode the fields? Or can we make a tool that will help a developer decode their fields? (Include an input that just decodes one encoded string (`atob()`))
100
- Q: If the fields are multiple choice and the developer's test picks at least one choice from that group of fields, can we use that to properly decode all the names of the fields? Hesitant to do this because some values may be encoded differently from other values (I think).
101
- Q: Is this more work than just refactoring how we group fields?
59
+ - Might be hard to avoid confusion about test behavior.
60
+ - Means developing both methods of testing. That's out of scope for one PR.
@@ -56,8 +56,6 @@ const server_install = async () => {
56
56
  session_vars.validateEnvironment();
57
57
 
58
58
  // 2. Prepare logs
59
- // Local devs: This will be separate from your test paths
60
- // unless you pass a `path` argument into the cucumber run command
61
59
  const argv = require(`minimist`)(process.argv.slice(2));
62
60
  const log = new Log({ path: argv.path, context, });
63
61
 
@@ -54,7 +54,7 @@ const setup = async () => {
54
54
  + `dainstall this package on your server. See above for more details.`;
55
55
  log.throw({ code: `ALK0047`, context,
56
56
  before: `☝️ ☝️ ☝️ Error data ☝️ ☝️ ☝️\n`,
57
- error: `Error while installing project` },
57
+ error: new Error(`Error while installing project on your docassemble server during GitHub+You™ tests`) },
58
58
  install_err_msg
59
59
  );
60
60
  }
@@ -68,8 +68,6 @@ const setup = async () => {
68
68
  session_vars.validateEnvironment();
69
69
 
70
70
  // 2. Prepare logs
71
- // Local devs: This will be separate from your test paths
72
- // unless you pass a `path` argument into the cucumber run command
73
71
  const argv = require(`minimist`)(process.argv.slice(2));
74
72
  const log = new Log({ path: argv.path, context, });
75
73
 
@@ -40,8 +40,8 @@ const takedown = async () => {
40
40
  session_vars.validateEnvironment();
41
41
 
42
42
  // 2. Prepare logs
43
- // Local devs: This will be separate from your test artifacts paths
44
- // unless you pass a `path` argument into the `cucumber` run command
43
+ // Local devs: This will be separate from your test artifacts paths unless you
44
+ // pass a `path` argument into the `cucumber` run command
45
45
  const argv = require(`minimist`)( process.argv.slice(2) );
46
46
  const log = new Log({ path: argv.path, context, });
47
47
 
package/lib/globals.js ADDED
@@ -0,0 +1,10 @@
1
+ let globals = {
2
+ // File name prefix ALKiln can use to identify random answer generator files
3
+ generators_prefix: `alkiln_random`,
4
+ generated_files_folder_name: `_alkiln_generated`,
5
+ }
6
+
7
+ module.exports = globals;
8
+
9
+ // Discuss: Would this be clearer as a config file instead? If not, change name
10
+ // to `constants`?
@@ -1,21 +1,37 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  const fs = require('fs');
4
+ const fast_glob = require(`fast-glob`);
4
5
  const path = require(`path`);
5
6
  const cucumber_api = require('@cucumber/cucumber/api');
6
7
 
8
+ const globals = require(`./globals`);
7
9
  const Log = require('./utils/Log');
8
10
  const session_vars = require('./utils/session_vars');
11
+ const set_sources_paths = require(`./utils/set_sources_paths`);
9
12
 
10
13
  const SUMMARY_FILENAME = `cucumber-report.txt`;
11
14
 
12
15
 
13
- function build_config( command_args ) {
16
+ function build_config() {
17
+ /** Use the command line arguments to build the cucumberjs config object.
18
+ *
19
+ * @param {string?} - Arguments passed to the command line.
20
+ *
21
+ * @returns {object} - cucumberjs config object
22
+ * */
14
23
  // Turn all the paths to the sources folders into paths to the feature files
15
24
  let sources_paths = session_vars.get_sources_paths();
16
25
  let feature_paths = [];
17
- for ( let source of sources_paths ) {
18
- feature_paths.push( `${ source }/*.feature` );
26
+
27
+ // Get all the paths for the right feature files (for cucumber to use)
28
+ for ( let sources_path of sources_paths ) {
29
+ let glob = fast_glob.sync(
30
+ `${ sources_path }/**/*.feature`,
31
+ // Ignore "generator" feature files that we used to generate actual tests
32
+ { ignore: [`${ sources_path }/${ globals.generators_prefix }*.feature`] }
33
+ );
34
+ feature_paths.push( ...glob );
19
35
  }
20
36
 
21
37
  const config = {
@@ -26,9 +42,10 @@ function build_config( command_args ) {
26
42
  retry: 1,
27
43
  }
28
44
 
45
+ const cli_args = require(`minimist`)( process.argv.slice(2) );
29
46
  // Normalize tag expressions, if there are any
30
47
  // Joining and splitting handles both tags surrounded with `""` or those without `""`
31
- let tags = command_args[`_`].join(` `).split(` `);
48
+ let tags = cli_args[`_`].join(` `).split(` `);
32
49
  // If there was no tag expression, `tags` is ['']. Remove that item.
33
50
  tags = tags.filter(function(entry) { return entry !== ""; })
34
51
  if ( tags.length > 0 ) {
@@ -56,46 +73,12 @@ function build_config( command_args ) {
56
73
 
57
74
  async function main() {
58
75
  /** Create the cucumber config, run the tests, create and
59
- * manipluate the output. */
76
+ * manipulate the output. */
60
77
 
61
78
  const environment = { cwd: process.cwd() };
62
79
  log.info({ code: `ALK0049`, context: `pre_run environment` },
63
80
  environment
64
81
  );
65
-
66
- // We'll use the user info to construct the possible paths to /sources
67
- let id = session_vars.get_user_id();
68
- // `project` is the name in the server docassemble "Project" page
69
- let project = session_vars.get_user_project_name();
70
-
71
- // process.argv is a list of strings of commands and args.
72
- // If using `npm run cucumber` (as opposed to the bin commands),
73
- // use `--` before `--sources=./foo`
74
- const argv = require(`minimist`)( process.argv.slice(2), {
75
- default: {
76
- sources: [
77
- `./docassemble/*/data/sources`, // In GitHub folder
78
- `/usr/share/docassemble/files/playgroundsources/${id}/${project}/*.feature`, // From playground without S3
79
- `/tmp/playgroundsources/${id}/${project}/*.feature` // From playground with S3
80
- ],
81
- }
82
- });
83
-
84
- // Make sure `sources` is always a list
85
- let sources_list = argv.sources;
86
- if ( typeof argv.sources === `string` ) {
87
- sources_list = [ argv.sources ];
88
- }
89
-
90
- let one_dir_exists = session_vars.set_sources_paths( sources_list );
91
- if ( !one_dir_exists ) {
92
- // If we don't throw, it'll be more confusing when no tests run
93
- log.throw({ code: `ALK0050`, context: `pre_run`,
94
- error: new ReferenceError(
95
- `ALKiln can't find your "sources" folder(s). See above for details.`
96
- ),
97
- });
98
- }
99
82
 
100
83
  const { runConfiguration } = await cucumber_api.loadConfiguration({
101
84
  provided: build_config( argv ),
@@ -122,9 +105,9 @@ async function main() {
122
105
 
123
106
  /** Write to files.
124
107
  *
125
- * Note: Don't add to the purely report-focused files. Those are files where
126
- * authors can focus on the info we give them, which they often miss in
127
- * the GitHub action.
108
+ * Note: Avoid adding to the purely report-focused files. Those are files
109
+ * where authors can focus on the info we give them, which they often miss
110
+ * in the GitHub action.
128
111
  */
129
112
 
130
113
  if ( success ) {
@@ -134,7 +117,7 @@ async function main() {
134
117
  } else {
135
118
  // Don't want to throw error here, so don't pass error object
136
119
  log.warn({ code: `ALK0055`, context: `post_run`, },
137
- `The tests ran into something unexpected. Look above for more details.`
120
+ `The tests ran into something unexpected. See warnings and errors above.`
138
121
  );
139
122
  }
140
123
  // Custom exit code. May overlap with other systems.
@@ -142,14 +125,12 @@ async function main() {
142
125
  const exitCode = success ? 0 : 50;
143
126
  process.exitCode = exitCode;
144
127
 
145
- // This is hardcoded information from `provide_config` above.
146
- // This code isn't used for anything other than detecting and removing color codes.
147
128
  let summary_file_name = SUMMARY_FILENAME;
148
129
  fs.readFile(summary_file_name, 'utf8', (read_error, text) => {
149
130
  if ( read_error ) {
150
131
  // Something else will probably show an error that's more relevant
151
132
  log.warn({ code: `ALK0056`, context: `post_run error`, },
152
- `Failed to read cucumber report`, read_error
133
+ `Failed to read the report of the 3rd party library, cucumberjs. There may be more information above or below.`, read_error
153
134
  );
154
135
  return;
155
136
  }
@@ -184,7 +165,13 @@ async function main() {
184
165
  try {
185
166
  fs.readFileSync(`${ log.path }/${ log.unexpected_filename }`); // Does it have contents?
186
167
  fs.appendFileSync(`${ log.path }/${ log.unexpected_filename }`, `\n\n\n\n\n` );
187
- } catch ( ignore_err ) { /* Missing unexpected file is ok */ }
168
+ } catch ( ignore_err ) {
169
+ /* Missing "unexpected results" file is ok */
170
+ }
171
+
172
+ // TODO: Is this meant to write to the debug file or is this debug info
173
+ // really supposed to be in the "unexpected results" file? Or are these
174
+ // comments old?
188
175
 
189
176
  // Add the heading for debug content
190
177
  fs.appendFileSync( `${ log.path }/${ log.unexpected_filename }`,
@@ -198,8 +185,8 @@ async function main() {
198
185
 
199
186
  } catch ( writing_cucumber_error ) {
200
187
  let code = `ALK0182`;
201
- let context = `post_run error`
202
- let msg = `Something probably went wrong earlier. Look above for more details. Skipped writing to ${ log.path }/${ log.debug_log_filename }.`;
188
+ let context = `post_run`
189
+ let msg = `Something probably went wrong earlier. See warnings and errors above. Skipped writing to ${ log.path }/${ log.debug_log_filename }.`;
203
190
  try {
204
191
  log.warn({ code, context, }, msg, writing_cucumber_error );
205
192
  } catch ( all_writing_error ) {
@@ -223,10 +210,10 @@ session_vars.validateEnvironment();
223
210
  // If using `npm run cucumber` (as opposed to the bin commands),
224
211
  // use `--` before `--sources=./foo`
225
212
  const argv = require(`minimist`)( process.argv.slice(2) );
226
- // The artifacts folder might be getting created for the first
227
- // time right here. The tests themselves should avoid creating
228
- // their own file.
229
213
  const log = new Log({ path: argv.path, context: `run` });
230
214
 
231
- // 3. run
215
+ // 3. Prep the sources paths to find .feature files
216
+ let sources_paths = set_sources_paths({ log });
217
+
218
+ // 4. run
232
219
  main();
package/lib/scope.js CHANGED
@@ -888,7 +888,7 @@ module.exports = {
888
888
  }, // Ends scope.normalizeTable()
889
889
 
890
890
  getAllFields: async function ( scope, { html }) {
891
- /* Given HTML string of a page, returns a list of all the fields in it
891
+ /** Given HTML string of a page, returns a list of all the fields in it
892
892
  * as some kind of sophisticated data? With their selectors, for example.
893
893
  * We will choose selectors that will continue to identify the node, eve
894
894
  * after a hidden field is revealed, etc.
@@ -1019,12 +1019,15 @@ module.exports = {
1019
1019
  // button, radio, checkbox yesno, and maybe others need the value of the `value` attribute
1020
1020
  let values = [];
1021
1021
  if ( node.name === `select` ) {
1022
+ // Get possible values
1022
1023
  values = await scope.getOptionValues( scope, { $select: $node });
1023
1024
  }
1024
1025
  if ( values.length === 0 && node.attribs.value ) {
1025
1026
  // TODO: Add links to docs about da YAML values that devs never fill in themselves and
1026
1027
  // so don't know what they are. E.g. yesnomabye -> True/False/None.
1027
1028
  // TODO: Text inputs and textareas must ignore the value, etc. How?
1029
+
1030
+ // Get the current value for not `select` fields
1028
1031
  values.push( node.attribs.value );
1029
1032
  }
1030
1033
 
@@ -1387,6 +1390,14 @@ module.exports = {
1387
1390
  * @param <field[n].guesses[n].trigger> {str} - var name that triggered this page
1388
1391
  *
1389
1392
  * @returns [ var_data[n] ]
1393
+ * @returns [{
1394
+ * table_row: var_data[n],
1395
+ * field: {
1396
+ * selector: str,
1397
+ * guesses: [{ values: [str] }]
1398
+ * }
1399
+ * }]
1400
+ * What does it return when there's no match?
1390
1401
  */
1391
1402
 
1392
1403
  let rows_that_match_field_names = [];
@@ -2118,6 +2129,38 @@ module.exports = {
2118
2129
  // await scope.page.mouse.move(1, 1); // Too big? Wait a while before drawing?
2119
2130
  await scope.page.mouse.up();
2120
2131
  },
2132
+
2133
+ get_page_id: async function ( scope ) {
2134
+ /** Return the current page's docassemble `question` block ID or `null`
2135
+ *
2136
+ * Note: The ID is already sanitized by docassemble.
2137
+ *
2138
+ * @param {object} scope - Tests state object
2139
+ * @param {object} scope.page - puppeteerjs page object
2140
+ *
2141
+ * @returns {null | string}
2142
+ * */
2143
+ let question_id_match = await scope.page.$eval(`body`, function (elem) {
2144
+ return elem.className.match(/question-([^ ]+)/);
2145
+ });
2146
+ if ( question_id_match !== null ) {
2147
+ return question_id_match[ 1 ];
2148
+ }
2149
+ return null;
2150
+ },
2151
+
2152
+ to_docassemble_sanitized_id: function ( risky_id ) {
2153
+ /** Return a version of `risky_id` that's sanitized the same way docassemble
2154
+ * sanitizes its IDs to turn them into HTML class names in python:
2155
+ * re.sub(r'[^A-Za-z0-9]+', '-', interview_status.question.id.lower())
2156
+ * (reference needed)
2157
+ *
2158
+ * @param {string} risky_id - The un-sanitized id
2159
+ *
2160
+ * @returns {string} - Sanitized id
2161
+ * */
2162
+ return risky_id.toLowerCase().replace(/[^A-Za-z0-9]+/g, '-');
2163
+ },
2121
2164
 
2122
2165
  examinePageID: async function ( scope, question_id = '' ) {
2123
2166
  /* Looks for a sanitized version of the question id as it's written