diff --git a/lib/docassemble/server_install.js b/lib/docassemble/server_install.js index 86a92346..8f3177d4 100755 --- a/lib/docassemble/server_install.js +++ b/lib/docassemble/server_install.js @@ -52,5 +52,5 @@ session_vars.validateEnvironment(); const argv = require(`minimist`)(process.argv.slice(2)); const log = new Log({ path: argv.path, context: `server_install` }); -// 3. run in a wrapper -log.with_cleanup({ todo: server_install }); +// 3. run +server_install(); diff --git a/lib/docassemble/setup.js b/lib/docassemble/setup.js index a28ebf5d..67934729 100755 --- a/lib/docassemble/setup.js +++ b/lib/docassemble/setup.js @@ -71,5 +71,5 @@ session_vars.validateEnvironment(); const argv = require(`minimist`)(process.argv.slice(2)); const log = new Log({ path: argv.path, context: `setup` }); -// 3. run in a wrapper -log.with_cleanup({ todo: setup }); +// 3. run +setup(); diff --git a/lib/docassemble/takedown.js b/lib/docassemble/takedown.js index 2ec4f7ef..02256363 100755 --- a/lib/docassemble/takedown.js +++ b/lib/docassemble/takedown.js @@ -42,5 +42,5 @@ session_vars.validateEnvironment(); const argv = require(`minimist`)( process.argv.slice(2) ); const log = new Log({ path: argv.path, context: `takedown` }); -// 3. run in a wrapper -log.with_cleanup({ todo: takedown }); +// 3. run +takedown(); diff --git a/lib/run_cucumber.js b/lib/run_cucumber.js index b83005ce..ec04cf21 100755 --- a/lib/run_cucumber.js +++ b/lib/run_cucumber.js @@ -197,5 +197,5 @@ const argv = require(`minimist`)( process.argv.slice(2) ); // their own file. const log = new Log({ path: argv.path, context: `run` }); -// 3. run in a wrapper -log.with_cleanup({ todo: main }); +// 3. run +main(); diff --git a/lib/steps.js b/lib/steps.js index 1fc12b4b..57923e67 100644 --- a/lib/steps.js +++ b/lib/steps.js @@ -1441,10 +1441,12 @@ After(async function(scenario) { } // Not sure if this is necessary - if ( changeable_test_status !== 'PASSED' ) { - log.stdout({ records_only: true }, changeable_test_status[0].toUpperCase() ); - } else { + if ( changeable_test_status === 'PASSED' ) { log.stdout({ records_only: true }, `.` ); + } else if ( changeable_test_status === `SKIPPED` ) { + log.stdout({ records_only: true }, `-` ); + } else { + log.stdout({ records_only: true }, changeable_test_status[0].toUpperCase() ); } let signout_succeeded = true; diff --git a/lib/utils/log.js b/lib/utils/log.js index f7f4155f..25321295 100644 --- a/lib/utils/log.js +++ b/lib/utils/log.js @@ -9,166 +9,57 @@ const files = require(`./files`); // TODO: return full message so it can be passed to manually // thrown errors if needed. -/** TODO: - * - Store log filename in runtime_config - * - `types` -> `context` - * - Remove "cosmetic"/"plain" logs and just work that beauty into the logs themselves - * To note: - * - report is pretty - * - report_log is not pretty, but has the same info as the report - * - verbose_log is very ugly - * - * Main question: What does the log interface look like? How do I capture these different pieces of information? - * - * More questions: - * - Should error logs also throw the errors? Otherwise you have to do - * that in 2 lines everywhere you throw an error. (There are ways to - * capture the stack to point to the relevant lines instead of the line - * that throws the error.) - * - How do we capture the logs from our GitHub composite actions in - * the same file as the other verbose logs (and also log them to the - * console, of course)? - * - How do we capture the running logs (e.g. `.`s) from cucumber? (We - * already know how to get the final results logs.) - * - Should we allow "in-between"s for all parts of the log? All together: - * pre-everything, icon/types/etc., pre-logs, logs (the actual messages to print), - * pre-data, data, post-data? We can default to nothing for all of those. No, - * the `logs` arg can take care of most of that. - * - Should we add a timestamp to each log? - * - How do we differentiate between methods that log to the console - * vs. methods that just store data? - * - * */ - -let log = {}; -module.exports = log; - -let create_msg_start = function ( types, pre='', code=`? ALK0000` ) { - // ? - unknown (fallback) - let types_str = types.join(` `); - if ( types_str.includes(`plain`) ) { - return pre; - } else { - return `${ code } ${ types_str }: ${ pre }`; - } -}; - -// Where is this used? -log.success = function ({ type='', pre='', data='', post='', code=`ALK0000` }) { - /** Log success msg with the given information and strings, prepended with `ALKiln`. - * `type` is often something like 'setup', 'takedown', etc. */ - // √ - info (fallback) - let start = create_msg_start([ type, `SUCCESS` ], pre, `🌈 ${code}` ); - // Log them each on a separate line - console.info( `${ start }` ); - if ( data ) { console.info( data ); } - if ( post ) { console.info( post ); } -}; - -// Where is this used? -log.info = function ({ type='', pre='', data='', post='', code=`ALK0000` }) { - /** Log info msg with the given information and strings, prepended with `ALKiln`. - * `type` is often something like 'setup', 'takedown', etc. */ - // & - info (fallback) - let start = create_msg_start([ type, `INFO` ], pre, `💡 ${code}` ); - - // // Log them each on a separate line - // console.info( `${ start }` ); - // if ( data ) { console.info( data ); } - // if ( post ) { console.info( post ); } -}; - -// Where is this used? -log.warn = function ({ type='', pre='', data='', post='', code=`ALK0000` }) { - /** Log warning msg with the given information and strings, prepended with `ALKiln`. - * `type` is often something like 'setup', 'takedown', etc. */ - // ! - warning (fallback) - let start = create_msg_start([ type, `WARNING` ], pre, `🔎 ${code}` ); - // Log them each on a separate line - console.warn( `${ start }` ); - if ( data ) { console.warn( data ); } - if ( post ) { console.warn( post ); } -}; - -// Where is this used? -log.error = function ({ type='', pre='', data='', post='', code=`ALK0000` }) { - /** Log error msg with the given information and strings, prepended with `ALKiln`. - * `type` is often something like 'setup', 'takedown', etc. */ - // X - error (fallback) - let start = create_msg_start([ type, `ERROR` ], pre, `🤕 ${code}` ); - // Log them each on a separate line with extra new lines around the whole thing - console.error( `\n${ start }` ); - if ( data ) { console.error( data ); } - if ( post ) { console.error( post, `\n` ); } -}; - -log.debug = function ({ type='', pre='', data='', post='', verbose=false, code=`ALK0000` }) { - /** If debugging is turned on, log a message that includes `ALKiln` and `debug` */ - // @ - debug (fallback) - let start = create_msg_start([ type, `DEBUG` ], pre, `🐛 ${ code }` ); - let full = `\n${ start }`; - if ( data ) { full += `\n${ data }`; } - if ( post ) { full += `\n${ post }`; } - - // // TODO: Make a verbose log instead of adding to the debug one and always add to it - // if ( verbose ) { - // log.add_to_debug_log( full ); - // } - - // if ( session_vars.get_debug() ) { - // // Log them each on a separate line - // console.log( `\n${ start }` ); - // console.log( data ); - // console.log( post ); - // } -}; - -// log.add_to_debug_log = function(value) { -// fs.appendFileSync(log.debug_log_file, value + `\n`); -// }; - -// function stripAnsi(string) { -// // This regex matches common ANSI escape sequences -// const ansiRegex = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g; -// return string.replace(ansiRegex, ''); -// } -// const output = []; - -log.debug_log_file = `debug_log.txt`; -log.verbose_log_file = `verbose_log.txt`; -log.unexpected_filename = `unexpected_results.txt`; - - -// =========================== -// =========================== -// =========================== - /** - * TODO next: - * - write tests...? - * - edit the decision doc? Too many individual decisions? Separate docs? + * TODO: + * - [x] Store log filename in runtime_config + * - [x] `types` -> `context` + * - [x] Remove "cosmetic"/"plain" logs and just work that beauty into the logs themselves + * - [ ] Save logs from GitHub actions as well + * - [ ] Capture cucumber ProgressFormatter logs instead of making them ourselves? + * - [ ] Write user errors and warnings (sent to .console()) to unexpected results file + * - [ ] Write debug errors and warnings to unexpected results file? + * - [ ] write tests...? + * - [ ] Edit the decision doc? Too many individual decisions? Use separate docs instead? * - streams vs. formatter vs. fs.appendFileSync * - general architecture? * - keeping report separate for now * - try/catching everything + collecting errors - * - `throw` (non-standard) + * - `throw` (non-standard) (throw errors in logs? (bad stack trace) 2 lines everywhere instead?) * - `success` (non-standard) - * - + * - `before` option and no others? (others can be included in message, but no way to log before the code otherwise.) + * - Name "debug" meaning change instead of "verbose" * */ // class Log { - /** Handles logging to the console and saving to files and reports. - * Required argument `{ path }` - the path where the final logs - * should be stored. + /** Handles logging to the console, saving to files and reports, + * and throwing errors. + * + * Use this to: + * - Log to the console + * - Save to the debug log + * - Throw errors with log codes and other info + * - Log without new lines * - * Keeps track of its own time differences if we need multiple - * loggers. It's a bit premature. This might not need to be a class. + * What are the different log files: + * - report is a pretty file that is only complete at the end of all tests + * - report_log is not pretty, but has the same info as the report in case tests end early + * - debug_log is very ugly and has notes for developers + * + * TODO: + * - Save to the report and report log + * + * @params + * options { object } + * options.path { str } - Optional. The path where the final logs + * should be stored. + * options.context { str } - Optional. Info about what's creating + * this Log. * * We're not using console.Console or write streams because we can't - * control their life cycle during the cucumber tests themselves. - * We can't tell when to properly flush and end the stream in the - * case of a sudden process failure. */ + * control their life cycle in steps.js. We can't tell when to + * properly flush and end the stream in the case of a sudden failure. + * */ report_filename = `report.txt`; report_log_filename = `report_log.txt`; @@ -187,78 +78,13 @@ class Log { this.path = path; session_vars.save_artifacts_path_name( this.path ); - // this.add_consoles({ path: this.path }); - if ( context ) { context = context + ` log`; } else { context = `log`; } this.info({ code: `ALK0215`, context, }, `Saving files to "${ this.path }"` ); - - // // Report - // this.report_path = `${ this.path }/${ this.report_filename }`; - // this.report = new Report(); - // Indentation? https://nodejs.org/docs/latest-v18.x/api/console.html#consolegrouplabel - } - - // add_consoles({ path }) { - // /** Note: These consoles add new lines after each write. - // * See https://nodejs.org/docs/latest-v18.x/api/console.html#new-consoleoptions */ - // // // This might go in the report obj itself. - // // const report_log_stream = fs.createWriteStream( - // // // Open stream in "append" mode - // // `${ path }/${ this.report_log_filename }`, { flags: `a` } - // // ); - // // this.report_console = new console.Console({ - // // stdout: report_log_stream, stderr: report_log_stream, - // // }); - - // this.streams = []; - - // // debug log - // const debug_log_stream = this.debug_log_stream = fs.createWriteStream( - // // Open stream in "append" mode - // `${ path }/${ this.debug_log_filename }`, { flags: `a` } - // ); - // this.debug_console = new console.Console({ - // stdout: debug_log_stream, stderr: debug_log_stream, - // }); - // this.streams.push( debug_log_stream ); - - // // TODO: report log - // // TODO: unexpected log? - - // this.streams_promises = []; - - // // Add promises and their listeners so we can close up properly at the end - // for ( let stream of this.streams ) { - // this.streams_promises.push( new Promise((end_with, throw_with) => { - // stream.once(`close`, end_with).once(`error`, throw_with); - // }) ); - // } - // } - - async with_cleanup({ todo }) { - // try { - await todo(); - // await this.clean_up(); - // } catch ( error ) { - // await this.clean_up(); - // throw error; - // } } - // async clean_up() { - // /** Make sure all the log's streams end cleanly */ - // // End every stream. The listeners are already listening - // for ( let stream of this.streams ) { - // stream.end(); - // } - // // Wait for all streams to finish closing - // await Promise.all(this.streams_promises); - // return this; - // } - console( console_opts = {}, ...logs ) { /** Log the logs to the console at the right level and with the metadata. * Return the start of the message - the part without the logs @@ -271,31 +97,34 @@ class Log { * - Should every log be able to have an error? To keep the * signature consistent. * - * Signature options: - * - * // Extra syntax - * log.error({ - * code: `123`, context: `setup internal`, - * logs: [`Error 1 message`, `Error 2 message`], // collected error messages - * error: error - * }); + * @examples: + * Log = require('./lib/utils/log.js'); + * log = new Log('_alkiln-'); * - * // Logs at end - * log.error( - * { code: `123`, context: `setup internal`, error: error }, - * `Error 1 message`, - * `Error 2 message` - * ); + * log.console(); + * // * ALK000c console LOG [2024-08-16 00:30:39UTC]: * - * // Logs at start - * log.error( - * `Error 1 message`, - * `Error 2 message` - * { code: `123`, context: `setup internal`, error: error }, - * ); + * log.console({ + * level: 'info', + * icon: '&', + * code: 'ALK00t1', + * context: 'a test', + * }, 'test info log'); * + * try { + * let czar = zoo + 5; + * } catch ( error ) { + * log.console({ + * level: 'error', + * before: '===\n===\n', + * icon: 'X' + * code: 'ALK00t2' + * context: 'err_test' + * error: new Error('This is an error'), + * }, 'error test log 1', 'error test log 2') + * } * */ - // This creates kind of a weird signature... + let { // Defaults level = `log`, before = ``, // different name? above? start_decorator? @@ -320,6 +149,7 @@ class Log { // Throw whatever we can throw if ( do_throw ) { let custom_msg = this._stringify_logs({ logs: [ metadata, ...logs ] }); + // TODO: Save to unexpected results file if ( error instanceof Error ) { console.log( before + custom_msg ); throw error; @@ -336,20 +166,19 @@ class Log { // Otherwise, log at the given level if console has that method (`log` by default) // If a non-standard level was passed in, like `success`, use `info` by default if ( console[ level ]) { + // TODO: Save `warn` to unexpected results file console[ level ]( before + metadata, ...with_error ); } else { console.info( before + metadata, ...with_error ); } } catch ( console_log_error ) { - // Fail silently - // (TODO: try/catch) - // Gather errors? + // Fail silently, gather errors try { this.debug({ level: `warn`, icon: `🔎`, code: `ALK0211`, context: `internal`, error: console_log_error - }, `Skipped logging a message with console.${ level }()`); + }, `Skipped saving a ${ level } message to the debug log`); } catch ( console_debug_error ) { console.warn( `🔎 ALK0217 internal WARNING: Skipped the same log two times consecutively`, console_debug_error, console_log_error ); } @@ -363,8 +192,11 @@ class Log { * of the log. If there are errors, just log them too. * Keep as much info from the caller as possible. Avoid throwing * errors if at all possible while giving the max info possible. + * Avoid logging to the visible console. + * + * TODO: Log warning with stack trace * - * Example: + * @examples: * * Log = require('./lib/utils/log.js') * log = new Log({ path: '_alkiln-' }) @@ -428,47 +260,13 @@ class Log { if ( fs_append_failed ) { console.warn( logging_error ); } else { - fs.appendFileSync( `${ this.path }/${ this.debug_log_filename }`, `\n` + logging_error ); + // TODO: Log warning with stack trace + fs.appendFileSync( `${ this.path }/${ this.debug_log_filename }`, `\n` + logging_error.stack ); } } } return formatted_log; - - // try { - // // Should we split formatting and saving to file into their own errors? - // metadata = this._format_metadata({ level, icon, code, context }); - - // // Formatting the whole log will get its own catch - // try { - // formatted_log = `${ before }${ metadata }` - // + `${ this._stringify_logs({ logs: with_error }) }`; - - // } catch ( formatting_error ) { - // console.warn( `🔎 ALKx0213 internal WARNING: Unexpected behavior formatting log in log.debug`, formatting_error ); - // formatted_log = this._try_to_return_some_log_string({ - // level, before, icon, code, context, logs, error - // }); - // } - // fs.appendFileSync( `${ this.path }/${ this.debug_log_filename }`, `\n` + formatted_log ); - // } catch ( debug_console_log_error ) { - // log_error = true; - // console.warn( `🔎 ALKx0212 internal WARNING: Unexpected behavior with log.debug`, debug_console_log_error ); - // } - - // // TODO - // if ( log_error ) { - // try { - // fs.appendFileSync( - // `${ this.path }/${ this.debug_log_filename }`, - // formatted_log - // ); - // } catch ( fs_append_error ) { - // console.warn( `🔎 ALKx0217 internal WARNING: Unexpected behavior with fs.appendFileSync`, fs_append_error ); - // } - // } - - // return formatted_log; } _format_metadata( metadata_opts = {} ) { @@ -500,15 +298,15 @@ class Log { if ( item !== `` ) { metadata_list.push(item); } } metadata = metadata_list.join(` `) + `: `; - } catch ( in_metadata_error ) { + } catch ( metadata_error ) { let msg = `Skipped creating metadata`; code = `ALK0220`; icon = `🔎`; context = `internal logs metadata`; try { - this.debug({ icon, level: `warn`, code, context }, msg ); + this.debug({ icon, level: `warn`, code, context, error: metadata_error }, msg ); } catch { - console.warn( `${ icon } ${ code } ${ context } WARNING [${ Date.now() }]: ${ msg }`, in_metadata_error ); + console.warn( `${ icon } ${ code } ${ context } WARNING [${ Date.now() }]: ${ msg }`, metadata_error ); } } return metadata; @@ -539,87 +337,22 @@ class Log { } // ends typeof log } // Ends for logs } catch ( stringify_error ) { - // TODO: Should `stringify_error` get recorded even when util.inspect works? try { + // TODO: Should `stringify_error` get recorded even when util.inspect works? stringified_logs = util.inspect( logs, { depth: 8, maxArrayLength: null, maxStringLength: null, }); } catch ( util_error ) { if ( stringified_logs ) { - stringified_logs += `Unable to stringify logs. Got:\n${ stringified_logs }\n${ util_error.trace }\n${ stringify_error }`; + stringified_logs += `Unable to stringify logs. Got:\n${ stringified_logs }\n${ util_error.stack }\n${ stringify_error.stack }`; } else { - stringified_logs = `Unable to stringify logs.\n${ util_error.trace }\n${ stringify_error }`; + stringified_logs = `Unable to stringify logs.\n${ util_error.stack }\n${ stringify_error.stack }`; } - // let fallback_logs = `Unable to stringify logs. `; - // if ( stringified_logs ) { - // fallback_logs += `Got: ${ stringified_logs }`; - // } - // fallback_logs += `\n${ util_error.trace }`; } } return stringified_logs; } // Ends Log._stringify_logs() - // _try_to_return_some_log_string({ - // level, before, icon, code, context, logs, error, - // }) { - // /** Logging had an error. Try building simpler and simpler - // * output in the hope of returning anything useful. At worst, - // * return a final error and return something generic. */ - // let final_error = ''; - - // // One by one, try returning whatever it's possible to return - // try { - // // Something wrong with the logic of the code before this? - // let data = [ level, before, icon, code, context, logs, error ]; - // return this._if_succeed_log_final_error({ data, final_error }); - // } catch ( internal_try_to_return_error2 ) { - // final_error = internal_try_to_return_error2; - // } - - // try { - // // Something wrong with the `error` object? - // let data = [ level, before, icon, code, context, logs ]; - // return this._if_succeed_log_final_error({ data, final_error }); - // } catch ( internal_try_to_return_error3 ) { - // final_error = internal_try_to_return_error3; - // } - - // try { - // // Something wrong with the `logs` object? - // let data = [ level, before, icon, code, context, error ]; - // return this._if_succeed_log_final_error({ data, final_error }); - // } catch ( internal_try_to_return_error4 ) { - // final_error = internal_try_to_return_error4; - // } - - // try { - // // Something wrong with both? These should all be strings - // // and we can't check absolutely every combination - // let data = [ level, before, icon, code, context ]; - // return this._if_succeed_log_final_error({ data, final_error }); - // } catch ( internal_try_to_return_error5 ) { - // final_error = internal_try_to_return_error5; - // } - - // // Still throwing errors? We did the best we could - // console.warn( final_error ); - // return `🔎 ALKx0214 internal WARNING: Unable to return any logs. Error: ${ final_error }`; - // } - - // _if_succeed_log_final_error({ data, final_error }) { - // /** There was a problem. If we can stringify this data, - // * we found the problem - it was the last thing we tried - - // * so log what broke and return what works. Otherwise, - // * there is still a problem and this will error correctly. */ - // // util.inspect can handle circular references - // let working_stringified_value = util.inspect( data, { depth: 8, maxArrayLength: null, maxStringLength: null, }); - // // If stringifying failed, we won't get here. If it succeeded, - // // we're done, so we can return a useful error. - // console.warn( final_error ); - // return working_stringified_value; - // } - _stringify_inline({ prev_line_type, log }) { /** Format an inline log. Mostly involves fiddling with * the start of the string. */ @@ -635,14 +368,14 @@ class Log { _stringify_object({ obj }) { /** Try to stringify an object a few different ways. Works for - * errors too. - * TODO: discuss using https://nodejs.org/docs/latest-v18.x/api/util.html#utilinspectobject-options. */ + * errors too. */ try { // A block of text (the object) always starts on a new line // Try to stringify the object. return `\n${ util.inspect( obj, { depth: 8, maxArrayLength: null, maxStringLength: null, } )}`; } catch ( outer_error ) { // If that fails, see if it has its own way of stringifying + // TODO: record these errors? try { return `\n${ obj }`; } catch ( inner_error ) { @@ -687,21 +420,6 @@ class Log { }, ...logs); } - // error( error_opts = {}, ...logs ) { - // /** Console log and throw error. */ - // let { - // code = `ALK000e`, before = ``, context = ``, - // error - // } = error_opts; - // return this.console({ - // level: `error`, icon: `🤕`, - // before, code, context, error, - // }, ...logs); - // } - - // TODO: Consider this interface instead of `error` - // `error` gives the wrong impression. It's not the same - // as console.error() throw( throw_opts = {}, ...logs ){ /** Console log and throw error. * The `error` option is required. */ @@ -716,32 +434,30 @@ class Log { } unexpected({ text = `` } = {}) { + /** TODO: Save to the unexpected results file. (Errors, maybe warnings) */ fs.appendFileSync(`${ this.path }/${ this.unexpected_filename }`, `\n${ text }` ); } - // stdout({ log = `` } = {}) { - // /** Prints in the console inline. How do we get these in - // * the debug logs? */ - // process.stdout.write( log ); - // // Also write it to the report and the debug log - // return log; - // } - stdout(stdout_opts = {}, ...logs) { - /** Prints in the console inline and saves to - * the debug log and the report log. */ + /** Prints in the console inline and saves to the debug log + * and the report log. + * + * @params + * options { obj } + * options.records_only { bool } - Whether to only save logs + * without printing them to the console. E.g. progress + * summary characters like those that cucumber is already + * printing to the console. + * */ let { records_only = false, } = stdout_opts; let whole_log = logs.join(` `); - // Always write to debug somehow - inline? new line? - // Always write to the report in a new section at the top + // Write to the debug and report files this._record_stdout({ logs }); - if ( records_only ) { - // don't write to the console - } else { + if ( !records_only ) { process.stdout.write( whole_log ); } @@ -749,11 +465,11 @@ class Log { } _record_stdout({ logs = [] }) { - /***/ + /** Save stdout output to various debug and report files. */ this.debug({ code: `ALK0219`, context: `stdout` }, ...logs); let whole_log = logs.join(` `); fs.appendFileSync( `${ this.path }/${ this.report_log_filename }`, whole_log ); - // TODO: write to report after metadata + // TODO: write to report after report metadata } clear() { @@ -771,41 +487,3 @@ class Log { } // Ends Log{} module.exports = Log; - -/** - * TODO: - * - [ ] maintain old debug log? (what did this mean?) - * - [x] actions debug_log -> report_log - * - [x] actions verbose_log -> debug_log - * - [ ] stdout.write() - * - * - * Note: Sometimes there's just one log - * - * Example: - * - * Log = require('./lib/utils/log.js') - * log = new Log('_alkiln-') - * - * log.console(); - * // ? ALK000c console LOG [2024-08-16 00:30:39UTC]: - * - log.console({ - level: `info`, - icon: `&&&`, - code: `ALK00t1`, - context: `a test`, - }, `test info log`); - - try { - let czar = zoo + 5; - } catch ( error ) { - log.console({ - level: `error`, - before: `===\n===\n`, - icon: `X` - code: `ALK00t2` - context: `err_test` - error: new Error(`This is an error`), - }, `error test log 1`, `error test log 2`) - }*/