All files / src TestLogger.ts

100% Statements 40/40
100% Branches 26/26
100% Functions 8/8
100% Lines 33/33

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125    3x 3x 3x   3x 3x                                                                   3x   23x                         23x                       23x                 3x 23x 23x 23x 23x 30x         4x   7x   1x   17x     17x       23x 138x   138x 35x   35x   23x 23x 291x   23x       138x 23x   23x       3x  
/* eslint-disable prettier/prettier */
/* eslint key-spacing: ["error", { align: { afterColon: true, beforeColon: false, on: "value" } }] */
import * as envalid from 'envalid';
import { InspectOptions, inspect } from 'util';
import JSONbig from 'json-bigint';
 
const logLevels = { debug: 2, error: 5, fatal: 6, info: 3, trace: 1, warn: 4 };
const logLevelLabels = ['', 'TRACE  ', 'DEBUG  ', 'INFO   ', 'WARNING', 'ERROR  ', 'FATAL  '];
 
type LogLevel = keyof typeof logLevels;
type LoggerEntry = [LogLevel, number];
 
/** The base log function */
export type LogFunction = (...args: unknown[]) => void;
 
/** Recorder messages is an Array of Objects of this type */
export type LoggedMessage = { level: LogLevel; message: unknown[] };
 
/** The base logger object */
export type Logger = { [l in LogLevel]: LogFunction };
 
/**
 * A unit test dedicated logger. Check
 * https://github.com/input-output-hk/cardano-js-sdk/tree/master/packages/util-dev#testlogger for details.
 */
export type TestLogger = Logger & { messages: LoggedMessage[]; reset: () => void };
 
type TestStream = { columns?: number; isTTY?: boolean; write: Function };
 
/** Options for createLogger */
export interface TestLoggerOptions {
  /** The environment variables to use. Default: process.env - Used to test TestLogger */
  env?: NodeJS.ProcessEnv;
 
  /** If true, the logger will records all the logged values. - Default: false */
  record?: boolean;
 
  /** The stream to write logs. Default: process.stdout - Used to test TestLogger */
  stream?: TestStream;
}
 
const getConfig = (env: NodeJS.ProcessEnv, stream?: TestStream) => {
  const { TL_ARRAY, TL_BREAK, TL_COLOR, TL_COMPACT, TL_DEPTH, TL_HIDDEN, TL_JSON, TL_LEVEL, TL_PROXY, TL_STRING } =
    envalid.cleanEnv(env, {
      TL_ARRAY:   envalid.num({ default: 100 }),
      TL_BREAK:   envalid.num({ default: stream?.columns ? stream?.columns - 33 : 90 }),
      TL_COLOR:   envalid.bool({ default: stream?.isTTY || false }),
      TL_COMPACT: envalid.num({ default: 3 }),
      TL_DEPTH:   envalid.num({ default: 2 }),
      TL_HIDDEN:  envalid.bool({ default: false }),
      TL_JSON:    envalid.bool({ default: false }),
      TL_LEVEL:   envalid.str({ choices: Object.keys(logLevels) as LogLevel[], default: 'fatal' }),
      TL_PROXY:   envalid.bool({ default: false }),
      TL_STRING:  envalid.num({ default: 1000 })
    });
 
  const inspectOptions: InspectOptions = {
    breakLength:     TL_BREAK || Number.POSITIVE_INFINITY,
    colors:          TL_COLOR,
    compact:         TL_COMPACT || false,
    depth:           TL_DEPTH || Number.POSITIVE_INFINITY,
    maxArrayLength:  TL_ARRAY || Number.POSITIVE_INFINITY,
    maxStringLength: TL_STRING || Number.POSITIVE_INFINITY,
    showHidden:      TL_HIDDEN,
    showProxy:       TL_PROXY,
    sorted:          true
  };
 
  return { inspectOptions, minHeight: logLevels[TL_LEVEL], useJSON: TL_JSON };
};
 
/**
 * Creates a new TestLogger
 *
 * @param options If `record` is equal to `true`, all logged values are recorded in logger.messages
 * @returns the newly created TestLogger
 */
export const createLogger = (options: TestLoggerOptions = {}) => {
  const { env, record, stream } = { env: process.env, stream: process.stdout as TestStream, ...options };
  const { minHeight, inspectOptions, useJSON } = getConfig(env, stream);
  const messages: LoggedMessage[] = [];
  const stringify = (data: unknown) => {
    switch (typeof data) {
      case 'bigint':
      case 'boolean':
      case 'number':
      case 'symbol':
        return data.toString();
      case 'string':
        return data;
      case 'undefined':
        return 'undefined';
      case 'object':
        if (useJSON) return JSONbig.stringify(data);
      // eslint-disable-next-line no-fallthrough
      case 'function':
        return inspect(data, inspectOptions);
    }
  };
 
  const getLogFunction = ([level, height]: LoggerEntry) => {
    const label = logLevelLabels[height];
 
    return (...message: unknown[]) => {
      if (record) messages.push({ level, message });
 
      if (height < minHeight) return;
 
      const line = message.map(stringify).join(' ');
      const now = new Date().toISOString().replace('T', ' ').replace('Z', '');
      const lines = line.split('\n').map((_) => `${now} ${label} ${_}\n`);
 
      stream.write(lines.join(''));
    };
  };
 
  const logger = Object.fromEntries((<LoggerEntry[]>Object.entries(logLevels)).map((_) => [_[0], getLogFunction(_)]));
  const reset = () => <void>(<unknown>(messages.length = 0));
 
  return <TestLogger>{ messages, reset, ...logger };
};
 
/** The default logger */
export const logger = createLogger();