@suffolklitlab/alkiln 5.13.2 → 5.13.3-feat-random

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