@suffolklitlab/alkiln 5.13.2 → 5.13.4

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 (39) hide show
  1. package/.github/workflows/github_server.yml +7 -7
  2. package/.github/workflows/playground.yml +1 -1
  3. package/CHANGELOG.md +32 -1
  4. package/action.yml +70 -28
  5. package/action_for_github_server/action.yml +61 -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 +347 -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 +3 -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 +101 -0
  15. package/docs/seprate_file_generation_GitHub_action.md +11 -0
  16. package/lib/docassemble/docassemble_api_interface.js +77 -51
  17. package/lib/docassemble/server_install.js +32 -9
  18. package/lib/docassemble/setup.js +40 -25
  19. package/lib/docassemble/takedown.js +33 -10
  20. package/lib/parse.js +36 -0
  21. package/lib/run_cucumber.js +115 -36
  22. package/lib/scope.js +69 -77
  23. package/lib/steps.js +70 -42
  24. package/lib/utils/Log.js +982 -0
  25. package/lib/utils/files.js +18 -11
  26. package/lib/utils/get_sources_paths.js +162 -0
  27. package/lib/utils/parse.js +1 -0
  28. package/lib/utils/proccess_command_line_args.js +267 -0
  29. package/lib/utils/reports.js +64 -17
  30. package/lib/utils/session_vars.js +34 -21
  31. package/lib/utils/time.js +44 -0
  32. package/package.json +1 -1
  33. package/tests/log_codes/check_codes.sh +2 -1
  34. package/tests/log_codes/log_code_expected_instances.txt +33 -9
  35. package/tests/unit_tests/getAllFields.test.js +5 -0
  36. package/tests/unit_tests/getMatchingRows.test.js +5 -0
  37. package/tests/unit_tests/log.test.js +581 -0
  38. package/tests/unit_tests/normalizeTable.test.js +5 -0
  39. package/lib/utils/log.js +0 -96
@@ -0,0 +1,982 @@
1
+ const fs = require(`fs`);
2
+ const util = require(`node:util`);
3
+ const session_vars = require( `./session_vars` );
4
+ const time = require(`./time`);
5
+ const files = require(`./files`);
6
+
7
+ // ? 🌈 💡 🔎 🤕 🐛
8
+ // Alternatives: 🩹 ❤️‍🩹 🚑 🖊️
9
+
10
+ /**
11
+ * Future issues:
12
+ * - [ ] Save logs from GitHub actions as well? Users can download them themselves, but this
13
+ * might not be clear to them
14
+ * - [ ] Capture cucumber ProgressFormatter logs instead of making them ourselves?
15
+ * - [ ] keeping report separate for now (can just be an issue to integrate)
16
+ * */
17
+
18
+ class Log {
19
+ /** This class includes methods to save meaningful messages with additional
20
+ * metadata to the debug files while tests run. Most of the rest of `Log`'s
21
+ * methods similarly save to the debug files and also log to the console. We
22
+ * use them only during processes that wrap the core framework code[^1],
23
+ * like setup and takedown. All these outputs let both us and authors debug
24
+ * their interviews, their tests, and this framework's code.
25
+ *
26
+ * Errors: For functionality that wraps core code[^1], `Log` also has a
27
+ * `.throw()` method, which is like a regular log method and then also
28
+ * throws an error. `.throw()` isn't for test failures, though. Meanwhile,
29
+ * `Log` tries to avoid throwing its own errors. That way, authors don't
30
+ * have to deal with this non-critical functionality. We save those errors
31
+ * to the debug files. See the docs about errors.
32
+ *
33
+ * The different log files:
34
+ * - debug_log.txt contains detailed info for internal debugging.
35
+ * - unexpected_results.txt contains detailed messages only about warnings and
36
+ * failures. It will only exist if an unexpected result happens.
37
+ * - To integrate:
38
+ * - report.txt is a pretty file that is only complete at the end of all
39
+ * tests.
40
+ * - report_log.txt is not formatted nicely. It saves the same info as the
41
+ * report while tests are running in case tests end early.
42
+ *
43
+ * Discuss: pros and cons of integrating saving other kinds of files using
44
+ * `Log`, like screenshots, downloaded files, etc.
45
+ *
46
+ * The tests have the most detailed examples.
47
+ *
48
+ * [^1]: Core code is code that handles the Steps that authors write in their
49
+ * `.feature` files.
50
+ * */
51
+
52
+ /** Debug filenames. */
53
+ debug_log_filename = `debug_log.txt`; // Should always exist
54
+ // This is only here so we can add it to the final unexpected results file
55
+ temp_unexpected_debug_log_filename = `temp_unexpected_results_debug_log.txt`; // Only created if needed
56
+
57
+ /** Report filenames. Reports aren't integrated yet. This is a good central
58
+ * location for all this information and for future development. */
59
+ report_log_filename = `report_log.txt`; // Not integrated, but used
60
+ // Regular report filename
61
+ report_filename = `report.txt`; // Not integrated, but used
62
+
63
+ /** Will have all unexpected results - from report and debug logs. */
64
+ unexpected_filename = `unexpected_results.txt`; // Only created if needed
65
+
66
+ constructor({ path, context=`` } = {}) {
67
+ /** Makes sure the right directory exists and instantiates class properties.
68
+ * GitHub actions will pass in a path that may already exist. Other
69
+ * processes, like ALKilnInThePlayground, don't have a path yet.
70
+ *
71
+ * ALKiln runs in multiple different environments that use a different order
72
+ * of operations to run the tests. See the documentation about ALKiln's 4
73
+ * environments. ALKiln's current GitHub composite action must be able to
74
+ * force a specific path for the artifacts folder so that the GitHb
75
+ * setup, run, and takedown process' `Log`s all store their logs in the
76
+ * same directory. This is the GitHub composite action flow:
77
+ *
78
+ * 1. Make a path name.
79
+ * 2. Pass the path to the setup script, which makes a `Log` for the first
80
+ * time using that path name. `Log`, in here creates the folder.
81
+ * 3. Pass the path to the run script, which makes a `Log` using that path
82
+ * name. The folder already exists, so `Log` uses that folder. The run
83
+ * script makes sure the ALKiln core code knows what folder to use.
84
+ * 4. Pass the path to the takedown script, which makes a `Log` using that
85
+ * path name. The `Log` uses that same folder.
86
+ *
87
+ * ALKiln's other environments only use the run script for each test run.
88
+ * The run process tells the core code what directory to use.
89
+ *
90
+ * The tests can only run sequentially right now, not in parallel, so there
91
+ * is no need to worry about locking the file and creating queues for
92
+ * logs from different tests to keep them from getting mixed together.
93
+ *
94
+ * @param { Object } options
95
+ * @param { string } options.path - Optional. The directory where it should
96
+ * store the final logs. We currently only use this argument with GitHub
97
+ * actions.
98
+ * @param { string } options.context - Optional. Info about what is creating
99
+ * this Log. For example, "setup".
100
+ *
101
+ * @example
102
+ * const Log = require("./lib/utils/Log.js");
103
+ * const log = new Log({ path: "_alkiln-123", context: "setup" });
104
+ * */
105
+
106
+ // GitHub actions will pass in a path that may already exist
107
+ if ( path ) { files.make_artifacts_folder( path ); }
108
+ // Other processes, like ALKilnInThePlayground, don't have a path
109
+ else { path = files.make_artifacts_folder(); }
110
+
111
+ this.path = path;
112
+ session_vars.save_artifacts_path_name( this.path );
113
+
114
+ // Has this `Log` ever failed to append to the debug file?
115
+ this.fs_append_to_debug_file_failed = false;
116
+
117
+ // Add `log` to the context and tell the author what the path is
118
+ if ( context ) { context = context + ` log`; }
119
+ else { context = `log`; }
120
+ this.info({ code: `ALK0214`, context, },
121
+ `Saving files to "${ this.path }"`
122
+ );
123
+ }
124
+
125
+ _console( console_opts = {}, ...logs ) {
126
+ /** `_console` should be called internally by other methods of Log. We use it
127
+ * in functionality that wraps around test runs, specifically setup,
128
+ * takedown, run_cucumber (before and after running the tests), and API calls
129
+ * that interact with the server at those times. It writes to the console at
130
+ * different levels and throws errors. If it's not throwing a given error, it
131
+ * returns the combined metadata and the logs as a formatted string.
132
+ *
133
+ * The `_console` method will:
134
+ * 1. Save the data to files with `log.debug()`
135
+ * 2. If `do_throw` is true, write data to the console and throw the error
136
+ * 3. Otherwise, write data to the console at the right level
137
+ * 4. Return a formatted string of the metadata and the given data
138
+ *
139
+ * Flow summary:
140
+ * .debug() ─► write to the console ─► throw or return
141
+ *
142
+ * Complex flow diagram:
143
+ * ┌───────────────────────────────────────┐
144
+ * │ ._console() │
145
+ * ├───────────────────────────────────────┤
146
+ * .throw() ──┐ │ .debug() (all) (formatted logs str) │
147
+ * │ │ get metadata string (all) │
148
+ * │ │ │
149
+ * ├────► get safe Error │
150
+ * ├────► console.error() with extra info │
151
+ * └────► throw Error │
152
+ * .success() ──┐ │ │
153
+ * │ │ │
154
+ * .info() ──╘════► console.info() logs ════════════╗ │
155
+ * │ ║ │
156
+ * .warn() ───────► console.warn() logs─────┐ ║ │
157
+ * │ ▼ ║ │
158
+ * │ return formatted logs string ◄══╝ │
159
+ * └───────────────────────────────────────┘
160
+ *
161
+ * @param { Object } console_opts - Required. Metadata and functional flags
162
+ * and data
163
+ * @param { success|info|warn|error } console_opts.level - Required. The log
164
+ * level. Functional and metadata. Defined by the internal caller.
165
+ * @param { string } console_opts.before - Optional. Text to add before the
166
+ * metadata of the log. Metadata. Default "".
167
+ * @param { string } console_opts.icon - Required. Unicode char put at the
168
+ * start of metadata. Makes the level more easy to see quickly. Defined by
169
+ * the internal caller.
170
+ * @param { string } console_opts.code - Required. Unique consecutive log id
171
+ * with the format ALK####. Defined by the original caller.
172
+ * @param { string } console_opts.context - Optional. Info about the context
173
+ * where the log was called. Metadata. Examples: "setup", "run",
174
+ * "takedown". Default "". Defined by the original caller.
175
+ * @param { Error|any } console_opts.error - Optional. Only include if
176
+ * `do_throw` is true. Otherwise, `error` must be `undefined`. The error
177
+ * or data to throw if `do_throw` is `true`. Before that throw, `_console`
178
+ * will write the before, metadata, and logs. Try to use an actual
179
+ * instance of Error. Otherwise, the stack trace will include this
180
+ * function, which is confusing. Avoid that. Default `undefined`. Defined
181
+ * by the original caller.
182
+ * @param { boolean } console_opts.do_throw - Optional. If `true` caller wants
183
+ * to throw an error error. Default `false`. Defined by the original
184
+ * caller.
185
+ * @param { ...any } logs - Optional. The logs to write to the console. Joined
186
+ * by " " or "\n" depending on the type of the log. Default `undefined`.
187
+ * Defined by the original caller.
188
+ *
189
+ * @returns { string } Formatted metadata, logs, and, if given, the error.
190
+ *
191
+ * @example
192
+ * log._console();
193
+ * // * ALK000c LOG [2024-08-16 00:30:39UTC]:
194
+ *
195
+ * @example
196
+ * log._console({
197
+ * level: "info",
198
+ * icon: "&",
199
+ * code: "ALK00d1",
200
+ * context: "demo",
201
+ * }, "demo log", "demo log 2");
202
+ * // & ALK00d1 demo INFO [2024-08-16 00:30:39UTC]: demo log demo log 2
203
+ *
204
+ * @example
205
+ * // Logged in the console, but not returned
206
+ * try {
207
+ * let causes_error = undefined_value + 5;
208
+ * } catch ( demo_error ) {
209
+ * log._console({
210
+ * level: "error",
211
+ * before: "===\n===\n",
212
+ * icon: "X"
213
+ * code: "ALK00d2"
214
+ * context: "err_demo"
215
+ * error: new Error("This is an error"),
216
+ * do_throw: true,
217
+ * }, "error demo log", { demo: "obj" } );
218
+ * }
219
+ * // ===
220
+ * // ===
221
+ * // X ALK00d2 err_demo ERROR [2024-08-16 00:30:39UTC]: error demo log
222
+ * // {
223
+ * // demo: "obj"
224
+ * // }
225
+ * // Error: This is an error
226
+ * // <the stack trace>
227
+ *
228
+ * For more examples, see the tests.
229
+ * */
230
+
231
+ // It's not any skin off our nose if these values are undefined and I think
232
+ // a warning for that would be too much noise. Internal developers should
233
+ // have enough information to be able to figure out what these mean and
234
+ // correct problems if needed.
235
+ let { // Defaults
236
+ // Metadata
237
+ level = `log`, // Usually defined by the internal caller, e.g. `.warn()`
238
+ before = ``, // Sometimes undefined
239
+ icon = `*`, // Usually defined by the internal caller, e.g. `.warn()`
240
+ code = `ALK000c`, // Usually defined by the original caller
241
+ context = ``, // Sometimes undefined
242
+ // Error functionality
243
+ error, // Sometimes undefined
244
+ do_throw = false, // Sometimes undefined
245
+ } = console_opts;
246
+
247
+ let formatted_str_to_return = this.debug({ level, before, icon, code, context, error, do_throw }, ...logs );
248
+
249
+ let metadata_str = this._format_metadata({ level, icon, code, context });
250
+
251
+ // Throw whatever we can throw
252
+ if ( do_throw ) {
253
+ error = this._get_Error_safely({ maybe_error: error });
254
+ // WARNING: If someone catches the error we throw, the console logs that
255
+ // get logged here before catching will be confusing.
256
+ try {
257
+ logs = logs || [];
258
+ console.error( `${before}${metadata_str}:`, ...logs );
259
+ } catch ( pre_throw_error ) {
260
+ this.debug({
261
+ icon: `🖊️`, code: `ALK0215`, context: `internal log`, level: `note`,
262
+ }, `Skipped logging metadata before throwing the error`, pre_throw_error );
263
+ }
264
+
265
+ if ( error instanceof Error ) {
266
+ throw error;
267
+ } else {
268
+ // non-error messages will get in here if do_throw is `true` Ensure the
269
+ // call stack gets created at this point.
270
+ // Discuss: Should we include the metadata in this error since we don't
271
+ // need to preserve a stack trace? That's inconsistent, though.
272
+ throw new Error( error );
273
+ }
274
+ }
275
+
276
+ // Avoid using custom-formatted logs and error here.
277
+ // Let the console show them as it wants.
278
+ try {
279
+ // log at the given level if console has that method (`log` by default).
280
+ // If a non-standard level was passed in, like `success`, use `info` by
281
+ // default.
282
+ if ( !console[ level ]) { level = `info`; }
283
+ console[ level ]( `${before}${metadata_str}:`, ...logs );
284
+
285
+ } catch ( console_log_error ) {
286
+ // Fail silently
287
+ this.debug({
288
+ icon: `🖊️`, code: `ALK0211`, context: `internal log`, level: `note`,
289
+ }, `Skipped saving a ${ level } message to the debug log`, console_log_error );
290
+ } finally {
291
+ return formatted_str_to_return;
292
+ }
293
+ }
294
+
295
+ debug( debug_opts = {}, ...logs ) {
296
+ /** Saves metadata and any logs in the debug file. When appropriate, also
297
+ * saves to the unexpected results file. Includes an Error if needed.
298
+ * Returns a formatted string version of the log with its metadata. Avoids
299
+ * throwing internal errors while giving the max info possible. Avoids
300
+ * writing to the visible console. Every log should come through here.
301
+ *
302
+ * Tries to make saved logs look as similar as possible to console logs.
303
+ *
304
+ * `.debug()` has the same signature as `._console()`, but saves to files
305
+ * instead of writing to the console.
306
+ *
307
+ * @params - Similar to `._console()`, except we expect to use `.debug()`
308
+ * directly as well as internally, while we expect to only use
309
+ * `._console()` internally.
310
+ *
311
+ * @returns { string } Same as `._console()`
312
+ *
313
+ * Discuss: Log warnings with stack trace? function stackTrace() { return
314
+ * (new Error()).stack; } and https://stackoverflow.com/a/41820537/14144258
315
+ * var mystring=require("util").inspect(error_object);
316
+ *
317
+ * Flow:
318
+ * get metadata
319
+ * get formatted logs as string (maybe with error)
320
+ * combine those
321
+ * save those to debug log
322
+ * maybe save those to unexpected results log
323
+ * return the formatted string
324
+ *
325
+ * @example
326
+ * log.debug()
327
+ * // 🐛 ALK000d DEBUG [2024-08-16 00:38:49UTC]:
328
+ * */
329
+
330
+ let {
331
+ level= `debug`,
332
+ before = ``,
333
+ icon = `🐛`,
334
+ code = `ALK000d`,
335
+ context = ``,
336
+ error, // Sometimes undefined
337
+ do_throw = false,
338
+ } = debug_opts;
339
+
340
+ // "warning" level and icon for for internal devs we use to avoid disrupting
341
+ // the info authors are looking for
342
+ if ( level === `note` ) { icon = `🖊️`; }
343
+
344
+ let formatted = `Skipped formatting all logs.`;
345
+ try {
346
+
347
+ let metadata_str = this._format_metadata({ level, icon, code, context });
348
+
349
+ // Get the whole formatted log message
350
+ formatted = `${ metadata_str }: Skipped formatting logs.`;
351
+ let logging_errors_msgs = [];
352
+ try {
353
+
354
+ // Use `with_error` to include the error in the debug and unexpected
355
+ // results logs
356
+ let with_error = this._combine_logs_and_possible_error({
357
+ logs, maybe_error: error, do_throw,
358
+ });
359
+
360
+ let stringified = this._stringify_logs( ...with_error );
361
+
362
+ // Question: Does `formatted` double log the error in the console?
363
+ // Answer: `._console` doesn't use `formatted`, just returns it.
364
+ formatted = `${ before }${ metadata_str }: ${ stringified }`;
365
+ } catch ( formatted_log_error ) {
366
+ formatted = `🖊️ ALK0212 internal debug NOTE [${ Date.now() }]: Skipped formatting debug logs.`;
367
+ this._save_to_debug_safely({ unexpected: true, text: formatted });
368
+ this._save_to_debug_safely({ unexpected: true, text: formatted_log_error.stack });
369
+ }
370
+
371
+ let unexpected = `warn error note`.includes( level );
372
+ this._save_to_debug_safely({ unexpected, text: formatted });
373
+
374
+ } catch ( debug_error ) {
375
+ formatted = `🖊️ ALK0217 internal debug NOTE [${ Date.now() }]: Skipped log.debug()\n${ debug_error.stack }`;
376
+ this._save_to_debug_safely({ unexpected: true, text: formatted });
377
+ this._save_to_debug_safely({ unexpected: true, text: debug_error.stack });
378
+ }
379
+
380
+ return formatted;
381
+ }
382
+
383
+ _format_metadata( metadata_opts = {} ) {
384
+ /** Formats and returns the log's metadata. Catch ANY error.
385
+ *
386
+ * @param { Object } metadata_opts - Required. Custom options for metadata
387
+ * @param { string } metadata_opts.level - Required. Level of the log (info,
388
+ * warn, etc).
389
+ * @param { string } metadata_opts.icon - Required. Icon that makes the
390
+ * level of the log more visible.
391
+ * @param { string } metadata_opts.code - Required. The unique id of the
392
+ * log.
393
+ * @param { string } metadata_opts.context - Optional. Default is `""`. More
394
+ * context about the caller. For example, "setup" or "takedown".
395
+ *
396
+ * @returns { string } The log's metadata.
397
+ *
398
+ * @example
399
+ * log._format_metadata({
400
+ * level: "fax",
401
+ * icon: "%",
402
+ * code: "ALK00d7",
403
+ * context: "demo"
404
+ * });
405
+ * // % ALK00d7 demo FAX [2024-08-16 00:30:39UTC]
406
+ * */
407
+ let {
408
+ level = `log`,
409
+ icon = `?`,
410
+ code = `ALK000m`,
411
+ context = ``,
412
+ } = metadata_opts;
413
+
414
+ let metadata_str = `Formatted log string placeholder: `;
415
+ try {
416
+
417
+ // For elapsed time, see
418
+ // https://nodejs.org/api/console.html#consoletimelabel
419
+ // Should this metadata use a raw timestamp?
420
+
421
+ // Transform the level name if necessary
422
+ let metadata_level = level;
423
+ if ( level === `warn` ) { metadata_level = `warning`; }
424
+ let level_caps = metadata_level.toUpperCase();
425
+ // Ignore items that are empty strings before combining everything
426
+ let metadata_list = [];
427
+ for ( let item of [ icon, code, context, level_caps, `[${ time.log_date() }]` ]) {
428
+ if ( item !== `` ) { metadata_list.push(item); }
429
+ }
430
+ metadata_str = metadata_list.join(` `);
431
+
432
+ } catch ( metadata_error ) {
433
+ this._save_to_debug_safely({ unexpected: true, text: `🖊️ ALK0219 internal log metadata NOTE [${ Date.now() }]: Skipped creating metadata.` });
434
+ this._save_to_debug_safely({ unexpected: true, text: metadata_error.stack });
435
+ }
436
+ return metadata_str;
437
+ }
438
+
439
+ _stringify_logs(...logs) {
440
+ /** Returns a string of the combined and formatted version of the list of
441
+ * logs by stringifying non-strings and joining all lots with separators.
442
+ * An error will show its full stack. A bit different than `console.log()`.
443
+ *
444
+ * @param { ...string } - Optional. Default is `""`. Logs to combine.
445
+ *
446
+ * @returns { string } Combined and formatted logs.
447
+ *
448
+ * @example
449
+ * log._stringify_logs("apples", "oranges", { obj_log: "bananas" }, "bears");
450
+ * // apples oranges
451
+ * // { obj_log: 'bananas' }
452
+ * // bears
453
+ * */
454
+
455
+ let stringified_logs = ``;
456
+ let prev_line_type = null;
457
+
458
+ try {
459
+ for ( let log of logs ) {
460
+ // Consider refactor:
461
+ // let start = _start_of_stringified_log({ prev_type, log })
462
+ // stringified_logs = start + inspect(...) or start + _get_safe_val() or ???
463
+
464
+ if ( log === null || typeof log !== `object` ) {
465
+ stringified_logs += this._stringify_inline({ prev_line_type, inline_log: log });
466
+ prev_line_type = `inline`;
467
+
468
+ } else {
469
+ // Objects, including errors
470
+ stringified_logs += this._stringify_object({ prev_line_type, obj_log: log });
471
+ prev_line_type = `block`;
472
+ } // ends typeof log
473
+ } // Ends for logs
474
+ } catch ( stringify_error ) {
475
+
476
+ try {
477
+ // Discuss: Should the above `stringify_error` get recorded even when
478
+ // `this._inspect()` works?
479
+ stringified_logs = this._inspect({ value: logs });
480
+ } catch ( util_error ) {
481
+ if ( stringified_logs.length > 0 ) {
482
+ stringified_logs += `Skipped stringifying logs. Got:\n${ stringified_logs }\n${ util_error.stack }\n${ stringify_error.stack }`;
483
+ } else {
484
+ stringified_logs = `Skipped stringifying logs.\n${ util_error.stack }\n${ stringify_error.stack }`;
485
+ }
486
+ }
487
+
488
+ }
489
+
490
+ return stringified_logs;
491
+ } // Ends Log._stringify_logs()
492
+
493
+ _stringify_inline({ prev_line_type, inline_log }) {
494
+ /** Formats the start of an inline log based on the previous log and returns
495
+ * that combined string.
496
+ *
497
+ * @param { "inline"|"block"|null } prev_line_type - Required. The type of
498
+ * the previous log.
499
+ * @param { null|non-obj } inline_log - Required. The log.
500
+ *
501
+ * @returns { string } The formatted log.
502
+ *
503
+ * @example
504
+ * // Backticks are to make the surrounding white space more clear
505
+ * log._stringify_inline({ prev_line_type: null, inline_log: "apple" });
506
+ * // `apple`
507
+ * log._stringify_inline({ prev_line_type: 'inline', inline_log: "apple" });
508
+ * // ` apple`
509
+ * log._stringify_inline({ prev_line_type: 'block', inline_log: "apple" });
510
+ * // `
511
+ * // apple`
512
+ * */
513
+ // An inline log's string starts with nothing if it's the first string...
514
+ let start = ``;
515
+ // ...starts after a space if its next to an inline string...
516
+ if ( prev_line_type === `inline` ) { start = ` `; }
517
+ // ...or starts on a new line if it's after a block of text, like an error
518
+ // or object.
519
+ else if ( prev_line_type === `block` ) { start = `\n`; }
520
+ return `${ start }${ inline_log }`;
521
+ }
522
+
523
+ _stringify_object({ prev_line_type, obj_log }) {
524
+ /** Return a string - either the object as a string or a default value with
525
+ * a "block"-type separator at the start. Can handle circular references.
526
+ * Catch ANY error.
527
+ *
528
+ * @param { Object } obj_log - Required. The object to format.
529
+ *
530
+ * @returns { string } The obj as a string plus a separator
531
+ *
532
+ * @example
533
+ * // Quotes are to make the surrounding white space more clear
534
+ * log._stringify_object({ obj_log: { fruit: "bananas" } });
535
+ * //
536
+ * // { fruit: 'bananas' }
537
+ * */
538
+ let start = ``;
539
+ if ( prev_line_type === `inline` || prev_line_type === `block` ) {
540
+ start = `\n`;
541
+ }
542
+
543
+ try {
544
+ // Try to stringify the object.
545
+ return `${ start }${ this._inspect({ value: obj_log }) }`;
546
+
547
+ // Discuss: record these errors?
548
+ } catch ( outer_error ) {
549
+ // If that fails, see if it has its own way of stringifying
550
+ try {
551
+ return `${ start }${ obj_log }`;
552
+ } catch ( inner_error ) {
553
+ // If everything else fails, give at least some information
554
+ return `${ start }Skipped logging this ${ typeof obj_log } which is an instance of ${ obj_log.constructor.name }`;
555
+ } // ends inner try
556
+ } // ends outer try
557
+ }
558
+
559
+ _combine_logs_and_possible_error({ logs=[], do_throw=false, maybe_error }) {
560
+ /** Returns a list of combined values to be logged. If we need to include
561
+ * an error, ensure it's an error. Catch ANY internal errors.
562
+ *
563
+ * @param { array } logs - Required. List of logs.
564
+ * @param { boolean } do_throw - Optional. Default is `false`. If `true`
565
+ * caller wants to throw an error.
566
+ * @param { Error|undefined } maybe_error - Optional. Default is
567
+ * `undefined`. A value that might be. turned into an Error and is
568
+ * definitely _safe_ to turn into an Error. For example, no circular
569
+ * references.
570
+ *
571
+ * @returns { array } List of all values that should be logged.
572
+ *
573
+ * @example
574
+ * let with_error = log._combine_logs_and_possible_error({
575
+ * [ "a", "b" ], do_throw: true, maybe_error: "A string."
576
+ * });
577
+ * // [ "a", "b", <An error with its stack and the message "A string."> ]
578
+ * @example
579
+ * let with_error = log._combine_logs_and_possible_error({
580
+ * [ "a", "b" ], do_throw: false, maybe_error: new Error("An error.")
581
+ * });
582
+ * // [ "a", "b" ]
583
+ * */
584
+ let combined = [ ...logs ];
585
+ try {
586
+ // If we're supposed to throw, make sure the error gets an Error value.
587
+ if ( do_throw ) {
588
+ combined.push( this._get_Error_safely({ maybe_error }) );
589
+
590
+ // Otherwise error should be `undefined`. If not, we get a warning.
591
+ } else if ( maybe_error !== undefined ) {
592
+ this._save_to_debug_safely({ unexpected: true, text: `🖊️ ALK0223 internal debug NOTE [${ Date.now() }]: Only logs that throw should give a value to \`error\`. Skipping the value.` });
593
+ }
594
+ } catch ( combining_error_error ) {
595
+ this._save_to_debug_safely({ unexpected: true, text: `🖊️ ALK0226 internal debug NOTE [${ Date.now() }]: Skipped combining the logs.` });
596
+ this._save_to_debug_safely({ unexpected: true, text: combining_error_error.stack });
597
+ }
598
+
599
+ return combined;
600
+ } // Ends _combine_logs_and_possible_error()
601
+
602
+ _get_Error_safely({ maybe_error }) {
603
+ /** Given anything, return an Error. Avoid throwing any errors.
604
+ *
605
+ * @param { Object } opts
606
+ * @param { * } opts.maybe_error - Optional. Can be anything.
607
+ *
608
+ * @returns { Error }
609
+ * */
610
+ if ( maybe_error instanceof Error ) {
611
+ return maybe_error;
612
+ } else {
613
+ if ( this._json_does_error( maybe_error )) {
614
+ maybe_error = this._get_inspected_val_safely( maybe_error );
615
+ }
616
+ // The stack trace will be less helpful, but it's all we can do if the
617
+ // caller hasn't already created the error.
618
+ return new Error( maybe_error );
619
+ }
620
+ }
621
+
622
+ _json_does_error( risky_value ) {
623
+ try {
624
+
625
+ // Possibly trigger an error (e.g. circular reference error) (new Error()
626
+ // with a circular reference can't be caught, it seems)
627
+ JSON.stringify( risky_value );
628
+ return false; // Did not trigger an error
629
+
630
+ } catch ( maybe_error_stringify_error ) {
631
+ this._save_to_debug_safely({ unexpected: true, text: `🖊️ ALK0224 internal log NOTE [${ Date.now() }]: Skipped JSON.stringify().` });
632
+ this._save_to_debug_safely({ unexpected: true, text: maybe_error_stringify_error.stack });
633
+ return true; // Triggered an error
634
+ }
635
+ }
636
+
637
+ _get_inspected_val_safely( risky_value ) {
638
+
639
+ let safe_val = `Skipped safe val.`;
640
+ try {
641
+ // If stringifying did create an error, try modifying it to make it
642
+ // safe. Not as good as the original, but better than nothing.
643
+ let inspected = this._inspect({ value: risky_value });
644
+ safe_val = inspected;
645
+
646
+ } catch ( risky_val_inspect_error ) {
647
+ this._save_to_debug_safely({ unexpected: true, text: `🖊️ ALK0222 internal log NOTE [${ Date.now() }]: Skipped inspecting risky value.` });
648
+ this._save_to_debug_safely({ unexpected: true, text: risky_val_inspect_error.stack });
649
+ // If even that failed, we can only return a vague message
650
+ safe_val = `ALKiln was unable to parse the value of \`error\`. Skipped parsing it.`;
651
+
652
+ } // ends try inspect
653
+
654
+ return safe_val;
655
+ }
656
+
657
+ _inspect({ value }) {
658
+ try {
659
+ return util.inspect( value, { depth: 8, maxArrayLength: null, maxStringLength: null, });
660
+ } catch ( direct_inspect_err ) {
661
+ return `🖊️ ALK0228 inspect log NOTE [${ Date.now() }]: Skipped util.inspect. ${ direct_inspect_err.stack }`;
662
+ }
663
+ }
664
+
665
+ /** Note: Functions that call `._console()` like those below only get used by
666
+ * setup, takedown, and run_cucumber, or a few exceptions, like unexpected
667
+ * behavior that gets in here that we cannot record any other way. */
668
+
669
+ success( success_opts = {}, ...logs ) {
670
+ /** Log a major successful operation to the console using additional
671
+ * metadata and the given logs. Use it sparingly. Use it in test suite
672
+ * wrapper functionality, like setup and takedown.
673
+ *
674
+ * @param success_opts { Object }
675
+ * @param success_opts.code { string } - Required. Unique log id of the
676
+ * format "ALK####", where "#" is a digit.
677
+ * @param success_opts.before { string } - Optional. Default is `""`.
678
+ * Decoration to log before the metadata.
679
+ * @param success_opts.context { string } - Optional. Default is `""`. Info
680
+ * about the origin of the logs.
681
+ * @param logs { ...string } - Optional. Default is `undefined`. Explanation
682
+ * of what happened.
683
+ *
684
+ * @returns { string } Formatted metadata, logs, and, if given, the error.
685
+ *
686
+ * @example
687
+ * log.warn({
688
+ * before: "~~~",
689
+ * code: "ALK00d3",
690
+ * context: "demo",
691
+ * }, "demo log");
692
+ * // ~~~🌈 ALK00d3 demo SUCCESS [2024-08-16 00:30:39UTC]: demo log
693
+ * */
694
+ // Discuss: do we want to log some successes as "💡 ... SUCCESS: " for
695
+ // smaller successful operations along the way?
696
+ let {
697
+ code = `ALK000s`, before = ``, context = ``
698
+ } = success_opts;
699
+ return this._console({
700
+ level: `success`, icon: `🌈`,
701
+ before, code, context,
702
+ }, ...logs);
703
+ }
704
+
705
+ info( info_opts = {}, ...logs ) {
706
+ /** Log informational messages to the console using additional metadata and
707
+ * the given logs. Use it in test suite wrapper functionality, like setup
708
+ * and takedown.
709
+ *
710
+ * @param info_opts { Object }
711
+ * @param info_opts.code { string } - Required. Unique log id of the format
712
+ * "ALK####", where "#" is a digit.
713
+ * @param info_opts.before { string } - Optional. Default is `""`.
714
+ * Decoration to log before the metadata.
715
+ * @param info_opts.context { string } - Optional. Default is `""`. Info
716
+ * about the origin of the logs.
717
+ * @param logs { ...string } - Optional. Default is `undefined`. Explanation
718
+ * of what happened.
719
+ *
720
+ * @returns { string } Formatted metadata, logs, and, if given, the error.
721
+ *
722
+ * @example
723
+ * log.warn({
724
+ * before: "~~~",
725
+ * code: "ALK00d4",
726
+ * context: "demo",
727
+ * }, "demo log");
728
+ * // ~~~💡 ALK00d4 demo INFO [2024-08-16 00:30:39UTC]: demo log
729
+ * */
730
+ let {
731
+ code = `ALK000i`, before = ``, context = ``,
732
+ } = info_opts;
733
+ return this._console({
734
+ level: `info`, icon: `💡`,
735
+ before, code, context,
736
+ }, ...logs);
737
+ }
738
+
739
+ warn( warning_opts = {}, ...logs ) {
740
+ /** Log warnings to the console using additional metadata and the given
741
+ * logs. Use it in test suite wrapper functionality, like setup and
742
+ * takedown.
743
+ *
744
+ * @param warning_opts { Object }
745
+ * @param warning_opts.code { string } - Required. Unique log id of the
746
+ * format "ALK####", where "#" is a digit.
747
+ * @param warning_opts.before { string } - Optional. Default is `""`.
748
+ * Decoration to log before the metadata.
749
+ * @param warning_opts.context { string } - Optional. Default is `""`. Info
750
+ * about the origin of the logs.
751
+ * @param logs { ...string } - Optional. Default is `undefined`. Explanation
752
+ * of what happened.
753
+ *
754
+ * @returns { string } Formatted metadata, logs, and, if given, the error.
755
+ *
756
+ * @example
757
+ * log.warn({
758
+ * before: "~~~",
759
+ * code: "ALK00d5",
760
+ * context: "demo",
761
+ * }, "demo log");
762
+ * // ~~~🔎 ALK00d5 demo WARNING [2024-08-16 00:30:39UTC]: demo log
763
+ * */
764
+ let {
765
+ code = `ALK000w`, before = ``, context = ``
766
+ } = warning_opts;
767
+ return this._console({
768
+ level: `warn`, icon: `🔎`,
769
+ before, code, context,
770
+ }, ...logs);
771
+ }
772
+
773
+ throw( throw_opts = {}, ...logs ){
774
+ /** Throw the given error option after writing metadata and logs to the
775
+ * console. Use it in test suite wrapper functionality, like setup and
776
+ * takedown. Helps us give more meaningful error messages and logs.
777
+ *
778
+ * @param throw_opts { Object }
779
+ * @param throw_opts.code { string } - Required. Unique log id of the format
780
+ * "ALK####", where "#" is a digit.
781
+ * @param throw_opts.before { string } - Optional. Default is `""`.
782
+ * Decoration to log before the metadata.
783
+ * @param throw_opts.context { string } - Optional. Default is `""`. Info
784
+ * about the origin of the logs.
785
+ * @param throw_opts.error { Error|any } - Required. Ideally, an `Error`. An
786
+ * `Error` keeps the stack trace from the line where the code created
787
+ * it, so the error message will be more clear. If this value is not an
788
+ * `Error`, `Log` will turn it into an `Error` before throwing it, which
789
+ * will make the stack trace harder to read.
790
+ * @param logs { ...string } - Optional. Default is `undefined`. Explanation
791
+ * of what happened.
792
+ *
793
+ * @returns { None } Throws an error, no return value.
794
+ *
795
+ * @example
796
+ * try {
797
+ * let causes_error = undefined_value + 5;
798
+ * } catch ( demo_error ) {
799
+ * log.throw({
800
+ * code: "ALK00d6"
801
+ * context: "err_demo"
802
+ * error: demo_error,
803
+ * }, "error demo log" );
804
+ * }
805
+ * // Logged in the console, but not returned
806
+ * // 🤕 ALK00d6 err_demo ERROR [2024-08-16 00:30:39UTC]: error demo log
807
+ * // Uncaught ReferenceError: undefined_value is not defined
808
+ * // <the stack trace>
809
+ * */
810
+ let {
811
+ code = `ALK000t`, before = ``, context = ``, error = `ALKiln default error`,
812
+ } = throw_opts;
813
+
814
+ this._console({
815
+ level: `error`, icon: `🤕`, do_throw: true,
816
+ before, code, context, error,
817
+ }, ...logs);
818
+ }
819
+
820
+ _save_to_debug_safely( save_debug_opts = {} ) {
821
+ /** Save to the debug file.
822
+ *
823
+ * @param { Object } save_debug_opts
824
+ * @param { string } save_debug_opts.text - Optional. Default is `""`. Str
825
+ * to save to the debug file.
826
+ * @param { boolean } save_debug_opts.unexpected - Optional. Default is
827
+ * false. Whether or not to save to the unexpected results file.
828
+ *
829
+ * @returns { string } The same value as given.
830
+ * */
831
+ let { text = ``, unexpected = false } = save_debug_opts;
832
+
833
+ // If we have ever failed to append to the debug log in the past, avoid
834
+ // doing so again and avoid logging an error message about it again.
835
+ try {
836
+ if ( this.fs_append_to_debug_file_failed ) {
837
+ console.info( text );
838
+ return text;
839
+ }
840
+ } catch ( console_log_failed_error ) {
841
+ this._warn_unable_to_log( console_log_failed_error );
842
+ return text;
843
+ }
844
+
845
+ try {
846
+ fs.appendFileSync( `${ this.path }/${ this.debug_log_filename }`, text + `\n` );
847
+ } catch ( fs_append_error ) {
848
+ // If this is our first time failing to write to the debug log, console
849
+ // warn about it.
850
+ this.fs_append_to_debug_file_failed = true;
851
+ console.warn(`🖊️ ALK0213 internal debug NOTE [${ Date.now() }]: Skipped appending to debug file.`);
852
+ console.warn( fs_append_error );
853
+ }
854
+
855
+ try {
856
+ if ( unexpected ) {
857
+ this._save_to_temp_unexpected_file({ text });
858
+ }
859
+ } catch ( error_sending_to_unexpected ) {
860
+ // Do nothing. These values will be in the debug log
861
+ }
862
+
863
+ return text;
864
+ }
865
+
866
+ _warn_unable_to_log( final_error ) {
867
+ /** The last ditch effort to leave a clue that something went horribly
868
+ * wrong.
869
+ *
870
+ * @param { any } final_error - Anything that can be logged to the console.
871
+ * */
872
+ try {
873
+ console.warn(
874
+ `🖊️ ALK0229 internal debug NOTE [${ Date.now() }]:`,
875
+ `Skipped logging data to console.`,
876
+ final_error
877
+ );
878
+ } catch ( not_even_console_can_handle_it ) {
879
+ // Nothing to do about it
880
+ }
881
+ }
882
+
883
+ _save_to_temp_unexpected_file({ text = `` } = {}) {
884
+ /** Save to the unexpected results file. Intended for errors and warnings.
885
+ * Catch ANY error.
886
+ *
887
+ * @param { Object } options
888
+ * @param { string } options.text - Optional. Default is `""`. Str to save
889
+ * to the unexpected results file.
890
+ *
891
+ * @returns { string } The same value as given.
892
+ * */
893
+ try {
894
+ fs.appendFileSync(`${ this.path }/${ this.temp_unexpected_debug_log_filename }`, `${ text }\n` );
895
+ } catch ( error_saving_to_unexpected_results_file ) {
896
+ // Do nothing. At worst, we'll get the data in the debug file or console .
897
+ }
898
+ return text;
899
+ }
900
+
901
+ /** `stdout` is currently for test progress summary and reports. */
902
+
903
+ stdout(stdout_opts = {}, ...logs) {
904
+ /** Prints in the console inline and saves to the debug log and, in future
905
+ * the report log. Use for progress summary during test runs and for
906
+ * reports. Logs are joined with no separator.
907
+ *
908
+ * @param { Object } stdout_opts
909
+ * @param { boolean } stdout_opts.records_only - Optional. Default is
910
+ * `false`. Whether to only save logs without printing them to the
911
+ * console. E.g. progress summary characters like those that cucumber is
912
+ * already printing to the console. logs { ...string } - Optional. Each
913
+ * argument must be a string.
914
+ * @param logs { ...string } - Optional. Default is `undefined`. Text to
915
+ * print.
916
+ *
917
+ * @returns { string } String of combined strings.
918
+ *
919
+ * @example
920
+ * log.stdout({ records_only: false }, ".", "." );
921
+ * // ..
922
+ * */
923
+ let {
924
+ records_only = false,
925
+ } = stdout_opts;
926
+ let whole_log = logs.join(``);
927
+
928
+ try {
929
+
930
+ // Write to the console
931
+ if ( !records_only ) {
932
+ process.stdout.write( whole_log );
933
+ }
934
+ // Write to the debug and report files
935
+ this._record_stdout( whole_log );
936
+ } catch ( stdout_error ) {
937
+ this._save_to_debug_safely({ unexpected: true, text: `🖊️ ALK0227 internal stdout NOTE [${ Date.now() }]: Skipped stdout.`});
938
+ this._save_to_debug_safely({ unexpected: true, text: stdout_error.stack });
939
+ };
940
+
941
+ return whole_log;
942
+ }
943
+
944
+ _record_stdout(...logs) {
945
+ /** Save stdout output to various debug and report files.
946
+ *
947
+ * @param { ...string } logs - Optional. Default is `undefined`. Strings to
948
+ * write to debug files.
949
+ *
950
+ * @returns { string } String of combined strings.
951
+ *
952
+ * @example
953
+ * log._record_stdout( ".", "." );
954
+ * // ..
955
+ * */
956
+ // Discuss: Return `log.debug()` value?
957
+ this.debug({ code: `ALK0220`, context: `stdout` }, ...logs);
958
+ let whole_log = logs.join(``);
959
+ // TODO: add this after the report heading (datetime of test, etc.)
960
+ fs.appendFileSync( `${ this.path }/${ this.report_log_filename }`, whole_log );
961
+ return whole_log;
962
+ }
963
+
964
+ clear() {
965
+ /** Empty all files possible without deleting them.
966
+ *
967
+ * @returns { Log } Self.
968
+ * */
969
+ try { fs.writeFileSync( `${ this.path }/${ this.debug_log_filename }`, `` ); }
970
+ catch (error) { /* Fail without throwing. Nice to have, not crucial. */ }
971
+ try { fs.writeFileSync( `${ this.path }/${ this.report_log_filename }`, `` ); }
972
+ catch (error) { /* Fail without throwing. Nice to have, not crucial. */ }
973
+ try { fs.writeFileSync( `${ this.path }/${ this.report_filename }`, `` ); }
974
+ catch (error) { /* Fail without throwing. Nice to have, not crucial. */ }
975
+ try { fs.writeFileSync( `${ this.path }/${ this.unexpected_filename }`, `` ); }
976
+ catch (error) { /* Fail without throwing. Nice to have, not crucial. */ }
977
+ return this;
978
+ }
979
+
980
+ } // Ends Log{}
981
+
982
+ module.exports = Log;