@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
@@ -21,20 +21,27 @@ files.readable_date = function() {
21
21
  return fancy_str;
22
22
  }; // Ends files.readable_date()
23
23
 
24
- files.make_artifacts_folder = function (folder_name) {
25
- /** Makes a folder in the root of the project, either with the given name
26
- * or with a default name.
27
- *
28
- * Works for local development as well, unlike action.yml or the workflow file.
29
- */
30
- if (folder_name) {
31
- folder_name = safe_filename( folder_name );
24
+ files.make_artifacts_folder = function (path) {
25
+ /** Makes a folder in the root of the project if it doesn't
26
+ * already exist, either with the given name or with a default name.
27
+ *
28
+ * Works for local development as well, without action.yml or workflow files.
29
+ * */
30
+ if (path) {
31
+ let name_parts = path.split(`/`);
32
+ let safe_parts = [];
33
+ for ( let part of name_parts ) {
34
+ safe_parts.push(safe_filename( part ));
35
+ }
36
+ path = safe_parts.join(`/`);
32
37
  } else {
33
- folder_name = `alkiln-${ files.readable_date() }`
38
+ path = `alkiln-${ files.readable_date() }`
34
39
  }
35
40
 
36
- fs.mkdirSync( folder_name );
37
- return folder_name;
41
+ // `recursive` avoids errors if folder already exists
42
+ // https://nodejs.org/docs/latest-v18.x/api/fs.html#fspromisesmkdirpath-options
43
+ fs.mkdirSync( path, { recursive: true });
44
+ return path;
38
45
  }; // Ends files.make_artifacts_folder()
39
46
 
40
47
  module.exports = files;
@@ -0,0 +1,16 @@
1
+ module.exports = function get_error_stacks({ errors }) {
2
+ /** Given a list of Error instances, return a list of their stacks
3
+ *
4
+ * Discuss: Move this into `Log`. Potentially print each stack with the
5
+ * same icon as the message. `{to_stack: [Errors]}.
6
+ *
7
+ * @param {object} obj - Named arguments
8
+ * @param {[Error]} obj.errors - A list of Error instances
9
+ *
10
+ * @returns {[Error.stack]} - A list of Error instances' `.stack` properties
11
+ * */
12
+ let stacks = errors.map(function( error ) {
13
+ return error.stack;
14
+ });
15
+ return stacks;
16
+ };
@@ -57,7 +57,7 @@ reports.addToReport = function( scope, { type, code, value }) {
57
57
  // thrown errors if needed.
58
58
 
59
59
  // Debugging logs
60
- let debug_line = getDebugLine({ type, code, value });
60
+ let debug_line = getDebugLine({ type, code, value, indent: global_indent });
61
61
  if ( session_vars.get_debug() ) {
62
62
  console.log( `Report: ${ debug_line }` );
63
63
  }
@@ -66,24 +66,43 @@ reports.addToReport = function( scope, { type, code, value }) {
66
66
  const MISC_ARTIFACTS_DIR = `_alkiln-misc_artifacts`;
67
67
  if ( !scope.paths ) {
68
68
  // Can this be usefully abstracted?
69
- scope.paths = { debug_log: `${ MISC_ARTIFACTS_DIR }/unit_tests_logs-${ files.readable_date() }.txt` };
69
+ let date = files.readable_date();
70
+ scope.paths = {
71
+ debug_log: `${ MISC_ARTIFACTS_DIR }/unit_tests_debug_log-${ date }.txt`,
72
+ report_log: `${ MISC_ARTIFACTS_DIR }/unit_tests_report_log-${ date }.txt`,
73
+ };
70
74
  }
71
75
 
72
76
  try {
73
77
  fs.appendFileSync( scope.paths.debug_log, `\nReport:\n${debug_line}` );
74
- } catch ( err ) {
78
+ fs.appendFileSync( scope.paths.report_log, `\nReport:\n${debug_line}` );
79
+ } catch ( file_error ) {
75
80
  fs.mkdirSync( MISC_ARTIFACTS_DIR );
76
81
  fs.appendFileSync( scope.paths.debug_log, `\nReport:\n${debug_line}` );
82
+ fs.appendFileSync( scope.paths.report_log, `\nReport:\n${debug_line}` );
77
83
  // An error in here should throw (for now)
78
84
  }
79
85
 
80
86
  // Report stuff
81
87
  let scenario = reports.makeSureScenarioExists( scope );
82
- scenario.lines.push({ type, code, value });
88
+ scenario.lines.push({ type, code, value, indent: global_indent });
83
89
 
84
- return getLineText({ type, code, value });
90
+ return getLineText({ type, code, value, indent: global_indent });
85
91
  }; // Ends reports.addToReport()
86
92
 
93
+
94
+ let indent_num = 0;
95
+ let global_indent = ``;
96
+ reports.indent = function() {
97
+ indent_num += 2;
98
+ global_indent = ` `.repeat( indent_num );
99
+ }
100
+ reports.outdent = function() {
101
+ indent_num = Math.max( 0, indent_num - 2 );
102
+ global_indent = ` `.repeat( indent_num );
103
+ }
104
+
105
+
87
106
  reports.create = function( scope ) {
88
107
  reports.makeSureReportExists( scope );
89
108
  }
@@ -115,25 +134,66 @@ reports.saveFinalScenarioData = function( scope, { status=`in progress` }) {
115
134
  this.setReportScenarioStatus( scope, { status });
116
135
  let scenario = scope.report.get( scope.scenario_id );
117
136
  let report = this.getPrintableScenario( scenario );
118
- if ( scenario.status === `FAILED` ) { this.failures.push( report ); }
119
- else if ( scenario.status === `PASSED` ) { this.successes.push( report ); }
120
- // else if ( scenario.status === `WARNING` ) { passed_reports += report; this.warnings.push( report ); }
121
- // else if ( scenario.status === `UNDEFINED` ) { passed_reports += report; this.undefineds.push( report ); }
122
- else { this.others.push( report ); }
123
-
124
- // Save to failures.txt (worry about optimization later)
125
- if ( this.failures.length > 1 || this.others.length > 1 ) {
126
- fs.writeFileSync(
127
- scope.paths.failures,
128
- this.title + this.getPrintableUnexpected( scope )
129
- );
137
+ if ( scenario.status === `PASSED` ) {
138
+ this.successes.push( report );
139
+ } else {
140
+ this._save_unexpected( scope, { status: scenario.status, report });
130
141
  }
142
+ // if ( scenario.status === `FAILED` ) {
143
+ // this.failures.push( report );
144
+ // }
145
+ // else if ( scenario.status === `PASSED` ) { this.successes.push( report ); }
146
+ // // else if ( scenario.status === `WARNING` ) { passed_reports += report; this.warnings.push( report ); }
147
+ // // else if ( scenario.status === `UNDEFINED` ) { passed_reports += report; this.undefineds.push( report ); }
148
+ // else { this.others.push( report ); }
149
+
150
+ // // Save to unexpected results files (worry about optimization later)
151
+ // if ( this.failures.length > 1 || this.others.length > 1 ) {
152
+ // let unexpected_results = this.title + this.getPrintableUnexpected( scope );
153
+ // fs.writeFileSync( scope.paths.report_of_unexpected, unexpected_results );
154
+ // fs.appendFileSync( scope.paths.debug_of_unexpected, unexpected_results );
155
+ // }
156
+
131
157
  // Save to reports.txt (worry about optimization later)
132
158
  fs.writeFileSync( scope.paths.report, this.getPrintableReport( scope ) );
133
159
 
134
160
  return scenario;
135
161
  }; // Ends reports.saveFinalScenarioData()
136
162
 
163
+ reports._save_unexpected = function(scope, { status, report }) {
164
+ /** Store the unexpected report and write to the unexpected report file.
165
+ *
166
+ * @param {Object} scope - State and methods of the current tests
167
+ * @param {Object} data
168
+ * @param {Object} data.status - Status of the current scenario
169
+ * @param {string} data.report - Report for the current scenario
170
+ * */
171
+ if ( status === `FAILED` ) {
172
+ this.failures.push( report );
173
+ // } else if ( status === `WARNING` ) { passed_reports += report; this.warnings.push( report );
174
+ // } else if ( status === `UNDEFINED` ) { passed_reports += report; this.undefineds.push( report ); }
175
+ } else {
176
+ this.others.push( report );
177
+ }
178
+
179
+ // If it's our first unexpected result, add the title to the top of the page
180
+ let already_unexpected = ``;
181
+ try { already_unexpected = fs.readFileSync( scope.paths.report_of_unexpected ); }
182
+ catch ( ignore_non_critical_error ) {}
183
+ if ( already_unexpected === `` ) {
184
+ try { fs.appendFileSync( scope.paths.report_of_unexpected, this.title ); }
185
+ catch ( ignore_non_critical_error ) {}
186
+ }
187
+
188
+ try { fs.appendFileSync( scope.paths.report_of_unexpected, report ); }
189
+ catch ( ignore_non_critical_error ) {}
190
+
191
+ return this;
192
+ }
193
+
194
+ reports.progress = function ({ logs=[] }) {
195
+ // TODO: save stdout-style to report_log, object, and getting printable scenario
196
+ };
137
197
 
138
198
  reports.setReportScenarioStatus = function( scope, { status=`PASSED` }) {
139
199
  /** Add final status of scenario to scenario report object - passed, failed, etc. */
@@ -197,38 +257,45 @@ reports.getPrintableScenario = function( scenario ) {
197
257
  /** Return a string generated from scenario list of report items. */
198
258
  let report = ``;
199
259
  for ( let line of scenario.lines ) {
200
- let { type, code, value } = line;
201
- report += `${ getLineText({ type, code, value }) }\n`;
260
+ let { type, code, value, indent } = line;
261
+ report += `${ getLineText({ type, code, value, indent }) }\n`;
202
262
  }
203
263
  return report;
204
264
  }; // Ends reports.getPrintableScenario()
205
265
 
206
- let getLineText = function ({ type=`no type`, code=`ALK0000`, value=`` }) {
207
- let start = ``;
266
+ let getLineText = function ({ type=`no type`, code=`ALK00rl`, value=``, indent=`` }) {
267
+
268
+ value = `${ value.replace( /\n/g, "\n" + indent ) }`;
269
+
270
+ if ( type === `as_is` ) { return `${ indent }${ value }`; }
271
+
272
+ let start = indent;
273
+ if ( type.includes(`outcome`) ) { start = `\n`; }
274
+
208
275
  if ( type.includes(`page_id`) ) { start += `screen id: `; }
209
- else if ( type.includes(`outcome`) ) { start += `\n`; } // extra new line
210
276
  else if ( type.includes(`warning`) ) { start += `🔎 ${ code } WARNING: `; }
211
277
  else if ( type.includes(`error`) ) { start += `\n🤕 ${ code } ERROR: `; } // extra new line
212
- return `${ start }${ value }`;
278
+ let result = `${ start }${ value }`;
279
+ return result;
213
280
  }; // Ends getLine()
214
281
 
215
- let getDebugLine = function ({ type=``, code=`ALK0000`, value=`` }) {
282
+ let getDebugLine = function ({ type=``, code=`ALK00rd`, value=``, indent=`` }) {
216
283
  /** In debug log lines, include all the information possible. */
217
284
 
218
285
  // `plain` is usually used for visual clarity and shouldn't have
219
286
  // codes and such associated with it. Return right away.
220
- if ( type.includes(`plain`) ) { return value; }
287
+ if ( type.includes(`plain`) ) { return `${ indent }${ value }`; }
221
288
 
222
- let start = ``;
289
+ let start = indent;
223
290
 
224
291
  // Get all the relevant emoji
225
- if ( type.includes(`debug`) ) { start += `🐞`; }
292
+ if ( type.includes(`debug`) ) { start += `🐛`; }
226
293
  if ( type.includes(`info`) ) { start += `💡`; }
227
294
  if ( type.includes(`success`) ) { start += `🌈`; }
228
295
  if ( type.includes(`warning`) ) { start += `🔎`; }
229
296
  if ( type.includes(`error`) ) { start += `🤕`; }
230
297
  // Default
231
- if ( start === `` ) { start += `?`; }
298
+ if ( start === indent ) { start += `?`; }
232
299
 
233
300
  start += ` ${ code }`;
234
301
 
@@ -3,19 +3,29 @@ const fg = require('fast-glob');
3
3
  const path = require('path');
4
4
  require('dotenv').config();
5
5
 
6
+ /** WARNING: Avoid importing other project files into here. Every other
7
+ * file should be able to reference this one without worrying
8
+ * about circular references.
9
+ * */
10
+
11
+ /** This file provides user info variable getters and setters.
12
+ * E.g. server address. It often uses `process.env` vars or
13
+ * vars that have been built from `process.env` vars. It is
14
+ * meant to avoid duplication, make testing easier, and leave
15
+ * room for future manipulation.
16
+ *
17
+ * No other file should need to use `process.env`.
18
+ *
19
+ * We use `.get_foo()` functions partly because it makes the code
20
+ * easier to test.
21
+ * */
22
+
6
23
  /**
7
- * This code assumes that this module is run as part of a GitHub Action. If the
8
- * customer is running it as a CLI tool, then they won't get useful messages yet.
9
- */
10
-
11
- /* This file provides user info variables. E.g. server address. They're either
12
- * `process.env` vars or vars that have been built from `process.env` vars.
13
- * It is meant to avoid duplication, make testing easier, and leave room for
14
- * future manipulation. No other file should need to use `process.env`.
15
- *
16
- * We have a bunch of `.get_foo()` functions partly because it makes the code
17
- * much easier to test.
18
- */
24
+ * Discuss: consider returning most logs (info, error, warning) so that the
25
+ * calling functions can use a `Log` to log them properly instead of logging
26
+ * to the console. Alternatively, move everything in here that `Log` uses
27
+ * into its own file so that `session_vars` can have its own `Log` instance.
28
+ * */
19
29
 
20
30
  const session_vars = {};
21
31
 
@@ -59,6 +69,18 @@ session_vars.get_setup_timeout_ms = function () {
59
69
  return timeout;
60
70
  };
61
71
 
72
+ session_vars.get_max_tests_per_generator_scenario = function() {
73
+ /** Returns the maximum number of tests to generate for any one generator
74
+ * Scenario.
75
+ *
76
+ * Assumes an average of 30 secs per test with a max of 20 min. for all tests
77
+ * for that one generator Scenario.
78
+ *
79
+ * @returns {int} - Max number of tests for one generator Scenario
80
+ * */
81
+ return process.env.MAX_RANDOM_TESTS_PER_SCENARIO || 40;
82
+ }
83
+
62
84
  session_vars.get_da_server_url = function () {
63
85
  try {
64
86
  let url = process.env.SERVER_URL || process.env.BASE_URL;
@@ -136,7 +158,7 @@ session_vars.save_project_name = function ( project_name ) {
136
158
  let file = fs.writeFileSync( runtime_config_path, JSON.stringify( json, null, 2 ) );
137
159
 
138
160
  if ( session_vars.get_debug() ) {
139
- console.log( `🐞 ALK0036 DEBUG: Stored name of Project "${ project_name }" locally.` );
161
+ console.debug( `🐛 ALK0036 DEBUG: Stored name of Project "${ project_name }" locally.` );
140
162
  }
141
163
  return file;
142
164
  }; // Ends save_project_name()
@@ -148,7 +170,7 @@ session_vars.get_project_name = function () {
148
170
  }
149
171
  let json = JSON.parse( fs.readFileSync( runtime_config_path ));
150
172
  let project_name = json[ project_name_key ] || null;
151
- if ( session_vars.get_debug() ) { console.log( `🐞 ALK0037 DEBUG: Project name from file is "${ project_name }".` ); }
173
+ if ( session_vars.get_debug() ) { console.debug( `🐛 ALK0037 DEBUG: Project name from file is "${ project_name }".` ); }
152
174
  return project_name;
153
175
  }; // Ends get_project_name()
154
176
 
@@ -172,29 +194,41 @@ session_vars.delete_project_name = function () {
172
194
  // const file_storing_artifact_path = `_alkiln_test_artifacts_dir_name.json`;
173
195
  const artifacts_path_key = `artifacts_path`;
174
196
  session_vars.save_artifacts_path_name = function ( path_name ) {
175
- /* Save a key with the name of the current artifacts folder in case it doesn't exist. */
197
+ /** Save a key with the name of the current artifacts folder
198
+ * in case it doesn't exist.
199
+ * */
176
200
  return session_vars.save_runtime_config_var({ key: artifacts_path_key, value: path_name });
177
201
  }; // Ends save_artifacts_path_name()
178
202
  session_vars.get_artifacts_path_name = function () {
179
- /* Get the name of the current artifacts path. */
203
+ /** Get the name of the current artifacts path. */
180
204
  return session_vars.get_runtime_config_var( artifacts_path_key );
181
205
  }; // Ends get_artifacts_path_name()
206
+ session_vars.delete_artifacts_path_name = function () {
207
+ /** Delete the runtime config value of the artifacts path. */
208
+ return session_vars.delete_runtime_config_var({ key: artifacts_path_key });
209
+ };
182
210
 
183
211
 
212
+ /** We use `da_install_method` for temp server vs. author's account (Playground)
213
+ * on author's server. For example, for deciding how to get the url of a
214
+ * server. See docs on 4 environments. TODO: Change `_method` to `_location`?
215
+ * */
184
216
  const install_method_key = `da_install_method`;
185
217
  session_vars.save_install_method = function ( install_method ) {
186
- /* Save a key with the name of the current artifacts folder in case it doesn't exist. */
218
+ /** Save a key with the name of the current artifacts folder in case it
219
+ * doesn't exist. */
187
220
  return session_vars.save_runtime_config_var({ key: install_method_key, value: install_method });
188
221
  }; // Ends save_install_method()
189
222
  session_vars.get_install_method = function () {
190
- /* Get the name of the current install method. */
223
+ /** Get the name of the current install method. */
191
224
  return session_vars.get_runtime_config_var( install_method_key );
192
225
  }; // Ends get_install_method()
193
226
 
194
227
 
195
228
  const repo_folder_key = `da_repo_folder_name`;
196
229
  session_vars.save_repo_folder_name = function (repo_folder_name) {
197
- /** Save a key with the name of the main package folder in the repository. */
230
+ /** Save a key with the name of the main package folder in the repository. For
231
+ * example, EvictionDefense in the docassemble-EvictionDefense folder. */
198
232
  return session_vars.save_runtime_config_var({ key: repo_folder_key, value: repo_folder_name });
199
233
  }; // Ends session_vars.save_repo_folder_name()
200
234
  session_vars.get_repo_folder_name = function () {
@@ -208,10 +242,11 @@ session_vars.save_runtime_config_var = function ({ key, value }) {
208
242
  * the json object there. The functions calling this are keeping
209
243
  * track of the key, so they're useful to keep around.
210
244
  *
211
- * @param key {str} Key to use to store `value` in runtime config file
212
- * @param value {any} Value to store in runtime config file
245
+ * @param {object} obj - Named arguments
246
+ * @param {str} obj.key - Key to use to store `value` in runtime config file
247
+ * @param {any} obj.value - Value to store in runtime config object
213
248
  *
214
- * @returns file {binary?} Runtime config file
249
+ * @returns {binary?} file Runtime config file
215
250
  */
216
251
 
217
252
  // Ensure runtime config file exists
@@ -226,82 +261,76 @@ session_vars.save_runtime_config_var = function ({ key, value }) {
226
261
  let file = fs.writeFileSync( runtime_config_path, JSON.stringify( json, null, 2 ) );
227
262
 
228
263
  if ( session_vars.get_debug() ) {
229
- console.log( `🐞 ALK0040 DEBUG: Stored runtime config var "${key}" locally as "${value}".` );
264
+ console.debug( `🐛 ALK0040 DEBUG: Stored runtime config var "${key}" locally as "${value}".` );
230
265
  }
231
266
  return file;
232
267
  }; // Ends session_vars.save_runtime_config_var()
233
268
 
234
269
  session_vars.get_runtime_config_var = function ( key ) {
235
270
  /** Get the value of the given key from the runtime config file.
236
- *
237
- * @param key {str} Key in runtime config file that contains the desired value
238
- * @returns value || null {any} Value of key in runtime config file. If it
239
- * doesn't exist, returns null.
240
- */
271
+ *
272
+ * @param {str} key - Key in runtime config file that contains the desired val
273
+ *
274
+ * @returns {any} - Value of given key in runtime config file or null
275
+ */
241
276
  try {
242
277
  let json = JSON.parse( fs.readFileSync( runtime_config_path ));
243
278
  let value = json[ key ];
244
279
  if ( session_vars.get_debug() ) {
245
- console.log( `🐞 ALK0041 DEBUG: Runtime config var "${key}" is "${value}".` );
280
+ console.debug( `🐛 ALK0041 DEBUG: Runtime config var "${ key }" is "${ value }".` );
246
281
  }
247
-
248
282
  return value;
249
- } catch ( error ) {
283
+ } catch ( get_var_error ) {
284
+
285
+ console.debug(`🖊️ ALK0271 NOTE: Skipped getting runtime config var "${ key }".`, get_var_error.stack );
250
286
  return null;
287
+
251
288
  }
252
289
  }; // Ends session_vars.get_runtime_config_var()
253
290
 
291
+ session_vars.delete_runtime_config_var = function ({ key }) {
292
+ /** Delete the runtime config var of the given `key`
293
+ *
294
+ * @param {object} obj - Named arguments
295
+ * @param {str} obj.key - Key in runtime config file object to delete
296
+ *
297
+ * @returns {null}
298
+ * */
299
+ try {
300
+ if ( !fs.existsSync( runtime_config_path )) { return null; }
301
+
302
+ let json = JSON.parse( fs.readFileSync( runtime_config_path ));
303
+ delete json[ key ];
304
+ fs.writeFileSync( runtime_config_path, JSON.stringify( json, null, 2 ));
305
+ return null;
254
306
 
255
- session_vars.set_sources_paths = function ( paths ) {
256
- /** Returns whether at least one of the given path exists. Store
257
- * a list of the existing paths. Warn of any missing paths. Can be
258
- * used for getting, e.g .feature files, files to upload, etc.
259
- *
260
- * Our code should ensure this gets run before cucumber tests get triggered.
261
- *
262
- * @param paths { [str] } List of paths to the 'sources' folder.
263
- *
264
- * @returns bool
265
- */
307
+ } catch ( delete_var_error ) {
266
308
 
267
- // Check every path, tracking which exist and which are missing
268
- let existing_paths = [];
269
- let missing_paths = [];
270
- for ( let path of paths ) {
271
- let dirs = fg.sync(
272
- [ path ],
273
- { onlyFiles: false, suppressErrors: true }
274
- );
275
-
276
- if ( dirs.length > 0 ) {
277
- existing_paths.push( ...dirs );
278
- } else {
279
- missing_paths.push( path );
280
- }
281
- }
309
+ console.debug( `🖊️ ALK0272 NOTE: Skipped deleting runtime config var "${ key }".`, delete_var_error.stack );
310
+ return null
282
311
 
283
- // If no paths were valid
284
- if ( existing_paths.length <= 0 ) {
285
- session_vars._sources_paths = null;
286
- return false;
287
312
  }
313
+ }; // Ends session_vars.delete_runtime_config_var()
288
314
 
289
- console.info(
290
- `🌈 ALK0042 SUCCESS: Did find "sources" file(s) at "${ existing_paths.join('", "') }".`
291
- )
292
315
 
293
- // Otherwise, store valid paths
294
- session_vars._sources_paths = existing_paths;
295
- return true;
316
+ session_vars.set_sources_paths = function ( paths ) {
317
+ /** Set the globally accessible value of the paths to the sources folder.
318
+ * Saves and returns the argument it was given.
319
+ *
320
+ * @param paths {[string]} List of paths to the 'sources' folder.
321
+ *
322
+ * @returns {null | [string]}
323
+ * */
324
+ session_vars._sources_paths = paths;
325
+ return paths || null;
296
326
  }; // End session_vars.set_sources_paths()
297
327
 
298
328
  session_vars.get_sources_paths = function () {
299
- /** Get the "sources" folder path. Should never be undefined during
300
- * a run of the test. run_cucumber.js should make sure it has
301
- * a default value.
302
- *
303
- * @returns str || null */
304
- return session_vars._sources_paths;
329
+ /** Get the previously stored "sources" folder paths.
330
+ *
331
+ * @returns {null | [string]}
332
+ * */
333
+ return session_vars._sources_paths || null;
305
334
  };
306
335
 
307
336
 
@@ -0,0 +1,98 @@
1
+ const fast_glob = require(`fast-glob`);
2
+
3
+ const session_vars = require(`./session_vars`);
4
+
5
+
6
+ module.exports = function set_sources_paths({ log }) {
7
+ /** Set the globally accessible value of the paths to the sources folder,
8
+ * where authors store tests and other relevant test files. Can throw an
9
+ * error.
10
+ *
11
+ * @param {object} obj - Named arguments
12
+ * @param {Log} obj.log - Logs and saves useful info for later debugging.
13
+ *
14
+ * @returns {[string]} - All paths found for package sources folders.
15
+ * */
16
+ session_vars.validateEnvironment();
17
+
18
+ let maybe_paths = get_potential_paths();
19
+
20
+ let sources_paths = get_sources_paths({ maybe_paths });
21
+ if ( sources_paths === 0 ) {
22
+ // If we don't throw, it'll be more confusing when no tests run
23
+ log.throw({ code: `ALK0050`, context: `sources`,
24
+ error: new ReferenceError(
25
+ `ALKiln can't find your "sources" folder(s) which should contain tests and their related files.`
26
+ ),
27
+ });
28
+ }
29
+
30
+ log.info({ code: `ALK0042`, context: `sources` },
31
+ `Did find "sources" file(s) at "${ sources_paths.join('", "') }".`
32
+ );
33
+ session_vars.set_sources_paths( sources_paths );
34
+
35
+ return sources_paths;
36
+ }
37
+
38
+
39
+ function get_potential_paths() {
40
+ /** Use command line args and session vars to return a list of strings of
41
+ * possible sources files paths.
42
+ *
43
+ * @returns {[string]} - Potential sources paths.
44
+ * */
45
+ // `project` is the name in the server docassemble "Project" page
46
+ let project = session_vars.get_user_project_name();
47
+ let id = session_vars.get_user_id();
48
+
49
+ // process.argv is a list of strings of commands and args.
50
+ // Internal note: If using `npm run cucumber`, use `--` before
51
+ // `--sources=./foo`
52
+ let cli_args = require(`minimist`)( process.argv.slice(2), {
53
+ default: {
54
+ sources: [
55
+ `./docassemble/*/data/sources`, // In GitHub folder
56
+ `/usr/share/docassemble/files/playgroundsources/${id}/${project}/*.feature`, // From playground without S3
57
+ `/tmp/playgroundsources/${id}/${project}/*.feature` // From playground with S3
58
+ ],
59
+ }
60
+ });
61
+
62
+ // Make sure `sources` is always a list
63
+ let potential_paths_list = cli_args.sources;
64
+ if ( !Array.isArray( cli_args.sources )) {
65
+ potential_paths_list = [ cli_args.sources ];
66
+ }
67
+
68
+ return potential_paths_list;
69
+ }
70
+
71
+
72
+ function get_sources_paths({ maybe_paths }) {
73
+ /** Of the given possible paths, return the paths that actually exist. Maybe
74
+ * none.
75
+ *
76
+ * @param {object} obj - Named arguments
77
+ * @param {[string]} obj.maybe_paths - All possible paths
78
+ *
79
+ * @returns {[string]} - List of all existing paths
80
+ * */
81
+ // Check every path, tracking which exist and which are missing
82
+ let existing_paths = [];
83
+ let missing_paths = [];
84
+ for ( let path of maybe_paths ) {
85
+ let dirs = fast_glob.sync(
86
+ [ path ],
87
+ { onlyFiles: false, suppressErrors: true }
88
+ );
89
+
90
+ if ( dirs.length > 0 ) {
91
+ existing_paths.push( ...dirs );
92
+ } else {
93
+ missing_paths.push( path );
94
+ }
95
+ }
96
+
97
+ return existing_paths;
98
+ }
package/lib/utils/time.js CHANGED
@@ -24,3 +24,47 @@ time.waitForTimeout = function( milliseconds ) {
24
24
  setTimeout(resolve, milliseconds);
25
25
  });
26
26
  };
27
+ time.sleep = time.waitForTimeout;
28
+ time.wait = time.waitForTimeout;
29
+
30
+ time.filename_date = function() {
31
+ /** Return the current timestamp in a human-readable
32
+ * filename-safe format to match the workflow artifact
33
+ * date format.
34
+ * See https://stackoverflow.com/a/48729396/14144258 and https://stackoverflow.com/a/6040556/14144258
35
+ */
36
+ // let date = new Date(); // now
37
+ // let day = ("0" + date.getUTCDate()).slice(-2);
38
+ // let month = ("0" + (date.getUTCMonth() + 1)).slice(-2);
39
+ // let year = date.getUTCFullYear();
40
+ // let hours = ("0" + date.getUTCHours()).slice(-2);
41
+ // let mins = ("0" + date.getUTCMinutes()).slice(-2);
42
+ // let secs = ("0" + date.getUTCSeconds()).slice(-2);
43
+
44
+ let dt = time.get_time_parts();
45
+ let safe_date = `${ dt.y }-${ dt.mon }-${ dt.d } at ${ dt.h }h${ dt.min }m${ dt.s }sUTC`;
46
+ return safe_date;
47
+ }; // Ends time.filename_date()
48
+
49
+ time.log_date = function () {
50
+ /** Return the current timestamp in a human-readable
51
+ * more compact log format.
52
+ * See https://stackoverflow.com/a/48729396/14144258 and https://stackoverflow.com/a/6040556/14144258
53
+ */
54
+ let dt = time.get_time_parts();
55
+ let short_date = `${ dt.y }-${ dt.mon }-${ dt.d } ${ dt.h }:${ dt.min }:${ dt.s }UTC`;
56
+ return short_date;
57
+ }; // Ends time.log_date()
58
+
59
+ time.get_time_parts = function () {
60
+ /** Get all the parts of the date as numbers. */
61
+ let date = new Date(); // now
62
+ return ({
63
+ y: date.getUTCFullYear(),
64
+ mon: ("0" + (date.getUTCMonth() + 1)).slice(-2),
65
+ d: ("0" + date.getUTCDate()).slice(-2),
66
+ h: ("0" + date.getUTCHours()).slice(-2),
67
+ min: ("0" + date.getUTCMinutes()).slice(-2),
68
+ s: ("0" + date.getUTCSeconds()).slice(-2),
69
+ });
70
+ }