@suffolklitlab/alkiln 5.13.2 → 5.13.3-feat-random-2

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