Files
2026-08-05 12:02:18 +02:00

314 lines
15 KiB
TypeScript

import assert from 'node:assert/strict';
import {Stream} from 'node:stream';
import {describe, it, beforeEach} from 'node:test';
/* eslint-disable no-console */
import {Log, markSensitive} from '../../lib/log';
import type {Log as LogType} from '../../lib/log';
describe('basic', function () {
let log: LogType;
describe('logging', function () {
let s: InstanceType<typeof Stream> & {
write: (m: string) => void;
writable: boolean;
isTTY: boolean;
end: () => void;
};
let result: string[] = [];
let logEvents: Array<{
id: number;
level: string;
prefix: string;
message: string;
timestamp?: unknown;
}> = [];
let logInfoEvents: typeof logEvents = [];
let logPrefixEvents: typeof logEvents = [];
const resultExpect = [
'\u001b[37;40mnpm\u001b[0m \u001b[0m\u001b[7msill\u001b[0m \u001b[0m\u001b[35msilly prefix\u001b[0m x = {"foo":{"bar":"baz"}}\n',
'\u001b[0m\u001b[37;40mnpm\u001b[0m \u001b[0m\u001b[36;40mverb\u001b[0m \u001b[0m\u001b[35mverbose prefix\u001b[0m x = {"foo":{"bar":"baz"}}\n',
'\u001b[0m\u001b[37;40mnpm\u001b[0m \u001b[0m\u001b[32minfo\u001b[0m \u001b[0m\u001b[35minfo prefix\u001b[0m x = {"foo":{"bar":"baz"}}\n',
'\u001b[0m\u001b[37;40mnpm\u001b[0m \u001b[0m\u001b[32;40mtiming\u001b[0m \u001b[0m\u001b[35mtiming prefix\u001b[0m x = {"foo":{"bar":"baz"}}\n',
'\u001b[0m\u001b[37;40mnpm\u001b[0m \u001b[0m\u001b[32;40mhttp\u001b[0m \u001b[0m\u001b[35mhttp prefix\u001b[0m x = {"foo":{"bar":"baz"}}\n',
'\u001b[0m\u001b[37;40mnpm\u001b[0m \u001b[0m\u001b[36;40mnotice\u001b[0m \u001b[0m\u001b[35mnotice prefix\u001b[0m x = {"foo":{"bar":"baz"}}\n',
'\u001b[0m\u001b[37;40mnpm\u001b[0m \u001b[0m\u001b[30;43mWARN\u001b[0m \u001b[0m\u001b[35mwarn prefix\u001b[0m x = {"foo":{"bar":"baz"}}\n',
'\u001b[0m\u001b[37;40mnpm\u001b[0m \u001b[0m\u001b[31;40mERR!\u001b[0m \u001b[0m\u001b[35merror prefix\u001b[0m x = {"foo":{"bar":"baz"}}\n',
'\u001b[0m\u001b[37;40mnpm\u001b[0m \u001b[0m\u001b[32minfo\u001b[0m \u001b[0m\u001b[35minfo prefix\u001b[0m x = {"foo":{"bar":"baz"}}\n',
'\u001b[0m\u001b[37;40mnpm\u001b[0m \u001b[0m\u001b[32;40mtiming\u001b[0m \u001b[0m\u001b[35mtiming prefix\u001b[0m x = {"foo":{"bar":"baz"}}\n',
'\u001b[0m\u001b[37;40mnpm\u001b[0m \u001b[0m\u001b[32;40mhttp\u001b[0m \u001b[0m\u001b[35mhttp prefix\u001b[0m x = {"foo":{"bar":"baz"}}\n',
'\u001b[0m\u001b[37;40mnpm\u001b[0m \u001b[0m\u001b[36;40mnotice\u001b[0m \u001b[0m\u001b[35mnotice prefix\u001b[0m x = {"foo":{"bar":"baz"}}\n',
'\u001b[0m\u001b[37;40mnpm\u001b[0m \u001b[0m\u001b[30;43mWARN\u001b[0m \u001b[0m\u001b[35mwarn prefix\u001b[0m x = {"foo":{"bar":"baz"}}\n',
'\u001b[0m\u001b[37;40mnpm\u001b[0m \u001b[0m\u001b[31;40mERR!\u001b[0m \u001b[0m\u001b[35merror prefix\u001b[0m x = {"foo":{"bar":"baz"}}\n',
'\u001b[0m\u001b[37;40mnpm\u001b[0m \u001b[0m\u001b[31;40mERR!\u001b[0m \u001b[0m\u001b[35m404\u001b[0m This is a longer\n',
'\u001b[0m\u001b[37;40mnpm\u001b[0m \u001b[0m\u001b[31;40mERR!\u001b[0m \u001b[0m\u001b[35m404\u001b[0m message, with some details\n',
'\u001b[0m\u001b[37;40mnpm\u001b[0m \u001b[0m\u001b[31;40mERR!\u001b[0m \u001b[0m\u001b[35m404\u001b[0m and maybe a stack.\n',
'\u001b[0m\u001b[37;40mnpm\u001b[0m \u001b[0m\u001b[31;40mERR!\u001b[0m \u001b[0m\u001b[35m404\u001b[0m \n',
'\u001b[0m\u001b[37;40mnpm\u001b[0m \u001b[0m\u0007noise\u001b[0m\u001b[35m\u001b[0m LOUD NOISES\n',
'\u001b[0m\u001b[37;40mnpm\u001b[0m \u001b[0m\u0007noise\u001b[0m \u001b[0m\u001b[35merror\u001b[0m erroring\n',
'\u001b[0m',
];
const logPrefixEventsExpect = [
{id: 2, level: 'info', prefix: 'info prefix', message: 'x = {"foo":{"bar":"baz"}}'},
{id: 11, level: 'info', prefix: 'info prefix', message: 'x = {"foo":{"bar":"baz"}}'},
{id: 20, level: 'info', prefix: 'info prefix', message: 'x = {"foo":{"bar":"baz"}}'},
];
const logInfoEventsExpect = logPrefixEventsExpect;
const logEventsExpect = [
{id: 0, level: 'silly', prefix: 'silly prefix', message: 'x = {"foo":{"bar":"baz"}}'},
{id: 1, level: 'verbose', prefix: 'verbose prefix', message: 'x = {"foo":{"bar":"baz"}}'},
{id: 2, level: 'info', prefix: 'info prefix', message: 'x = {"foo":{"bar":"baz"}}'},
{id: 3, level: 'timing', prefix: 'timing prefix', message: 'x = {"foo":{"bar":"baz"}}'},
{id: 4, level: 'http', prefix: 'http prefix', message: 'x = {"foo":{"bar":"baz"}}'},
{id: 5, level: 'notice', prefix: 'notice prefix', message: 'x = {"foo":{"bar":"baz"}}'},
{id: 6, level: 'warn', prefix: 'warn prefix', message: 'x = {"foo":{"bar":"baz"}}'},
{id: 7, level: 'error', prefix: 'error prefix', message: 'x = {"foo":{"bar":"baz"}}'},
{id: 8, level: 'silent', prefix: 'silent prefix', message: 'x = {"foo":{"bar":"baz"}}'},
{id: 9, level: 'silly', prefix: 'silly prefix', message: 'x = {"foo":{"bar":"baz"}}'},
{id: 10, level: 'verbose', prefix: 'verbose prefix', message: 'x = {"foo":{"bar":"baz"}}'},
{id: 11, level: 'info', prefix: 'info prefix', message: 'x = {"foo":{"bar":"baz"}}'},
{id: 12, level: 'timing', prefix: 'timing prefix', message: 'x = {"foo":{"bar":"baz"}}'},
{id: 13, level: 'http', prefix: 'http prefix', message: 'x = {"foo":{"bar":"baz"}}'},
{id: 14, level: 'notice', prefix: 'notice prefix', message: 'x = {"foo":{"bar":"baz"}}'},
{id: 15, level: 'warn', prefix: 'warn prefix', message: 'x = {"foo":{"bar":"baz"}}'},
{id: 16, level: 'error', prefix: 'error prefix', message: 'x = {"foo":{"bar":"baz"}}'},
{id: 17, level: 'silent', prefix: 'silent prefix', message: 'x = {"foo":{"bar":"baz"}}'},
{id: 18, level: 'silly', prefix: 'silly prefix', message: 'x = {"foo":{"bar":"baz"}}'},
{id: 19, level: 'verbose', prefix: 'verbose prefix', message: 'x = {"foo":{"bar":"baz"}}'},
{id: 20, level: 'info', prefix: 'info prefix', message: 'x = {"foo":{"bar":"baz"}}'},
{id: 21, level: 'timing', prefix: 'timing prefix', message: 'x = {"foo":{"bar":"baz"}}'},
{id: 22, level: 'http', prefix: 'http prefix', message: 'x = {"foo":{"bar":"baz"}}'},
{id: 23, level: 'notice', prefix: 'notice prefix', message: 'x = {"foo":{"bar":"baz"}}'},
{id: 24, level: 'warn', prefix: 'warn prefix', message: 'x = {"foo":{"bar":"baz"}}'},
{id: 25, level: 'error', prefix: 'error prefix', message: 'x = {"foo":{"bar":"baz"}}'},
{id: 26, level: 'silent', prefix: 'silent prefix', message: 'x = {"foo":{"bar":"baz"}}'},
{
id: 27,
level: 'error',
prefix: '404',
message: 'This is a longer\nmessage, with some details\nand maybe a stack.\n',
},
{id: 28, level: 'noise', prefix: '', message: 'LOUD NOISES'},
{id: 29, level: 'noise', prefix: 'error', message: 'erroring'},
];
beforeEach(function () {
result = [];
logEvents = [];
logInfoEvents = [];
logPrefixEvents = [];
log = new Log();
s = Object.assign(new Stream(), {
write: (m: string) => result.push(m),
writable: true,
isTTY: true,
end: () => {},
}) as typeof s;
log.stream = s as any;
log.heading = 'npm';
});
it('should work', function () {
assert.strictEqual(log.stream, s);
log.on('log', logEvents.push.bind(logEvents) as any);
log.on('log.info', logInfoEvents.push.bind(logInfoEvents) as any);
log.on('info prefix', logPrefixEvents.push.bind(logPrefixEvents) as any);
console.error('log.level=silly');
log.level = 'silly';
log.silly('silly prefix', 'x = %j', {foo: {bar: 'baz'}});
log.verbose('verbose prefix', 'x = %j', {foo: {bar: 'baz'}});
log.info('info prefix', 'x = %j', {foo: {bar: 'baz'}});
log.timing('timing prefix', 'x = %j', {foo: {bar: 'baz'}});
log.http('http prefix', 'x = %j', {foo: {bar: 'baz'}});
log.notice('notice prefix', 'x = %j', {foo: {bar: 'baz'}});
log.warn('warn prefix', 'x = %j', {foo: {bar: 'baz'}});
log.error('error prefix', 'x = %j', {foo: {bar: 'baz'}});
log.silent('silent prefix', 'x = %j', {foo: {bar: 'baz'}});
console.error('log.level=silent');
log.level = 'silent';
log.silly('silly prefix', 'x = %j', {foo: {bar: 'baz'}});
log.verbose('verbose prefix', 'x = %j', {foo: {bar: 'baz'}});
log.info('info prefix', 'x = %j', {foo: {bar: 'baz'}});
log.timing('timing prefix', 'x = %j', {foo: {bar: 'baz'}});
log.http('http prefix', 'x = %j', {foo: {bar: 'baz'}});
log.notice('notice prefix', 'x = %j', {foo: {bar: 'baz'}});
log.warn('warn prefix', 'x = %j', {foo: {bar: 'baz'}});
log.error('error prefix', 'x = %j', {foo: {bar: 'baz'}});
log.silent('silent prefix', 'x = %j', {foo: {bar: 'baz'}});
console.error('log.level=info');
log.level = 'info';
log.silly('silly prefix', 'x = %j', {foo: {bar: 'baz'}});
log.verbose('verbose prefix', 'x = %j', {foo: {bar: 'baz'}});
log.info('info prefix', 'x = %j', {foo: {bar: 'baz'}});
log.timing('timing prefix', 'x = %j', {foo: {bar: 'baz'}});
log.http('http prefix', 'x = %j', {foo: {bar: 'baz'}});
log.notice('notice prefix', 'x = %j', {foo: {bar: 'baz'}});
log.warn('warn prefix', 'x = %j', {foo: {bar: 'baz'}});
log.error('error prefix', 'x = %j', {foo: {bar: 'baz'}});
log.silent('silent prefix', 'x = %j', {foo: {bar: 'baz'}});
log.error('404', 'This is a longer\nmessage, with some details\nand maybe a stack.\n');
log.addLevel('noise', 10000, {bell: true});
(log as any).noise(false, 'LOUD NOISES');
(log as any).noise('error', 'erroring');
assert.strictEqual(result.join('').trim(), resultExpect.join('').trim());
const withoutTimestamps = (x: typeof logEvents) =>
x.map((m) => {
assert.strictEqual(Boolean((m as any).timestamp), true);
const copy = JSON.parse(JSON.stringify(m));
delete copy.timestamp;
return copy;
});
assert.deepStrictEqual(withoutTimestamps(log.record as any), logEventsExpect);
assert.deepStrictEqual(withoutTimestamps(logEvents), logEventsExpect);
assert.deepStrictEqual(withoutTimestamps(logInfoEvents), logInfoEventsExpect);
assert.deepStrictEqual(withoutTimestamps(logPrefixEvents), logPrefixEventsExpect);
});
});
describe('utils', function () {
it('enableColor', function () {
log.enableColor();
assert.ok((log as any)._format('x', {fg: 'red'}).includes('\u001b'));
});
it('disableColor', function () {
log.disableColor();
assert.strictEqual((log as any)._format('x', {fg: 'red'}), 'x');
});
it('_buffer while paused', function () {
log.pause();
log.log('verbose', 'test', 'test log');
assert.strictEqual(log._buffer.length, 1);
log.resume();
assert.strictEqual(log._buffer.length, 0);
});
});
describe('log.log', function () {
beforeEach(function () {
log = new Log();
});
it('emits error on bad loglevel', async function () {
await new Promise<void>((resolve, reject) => {
log.once('error', (err: Error) => {
assert.strictEqual(/Undefined log level: "asdf"/.test(String(err)), true);
resolve();
});
log.log('asdf', '', 'bad loglevel');
setTimeout(() => reject(new Error('timeout')), 1000);
});
});
it('resolves stack traces to a plain string', async function () {
await new Promise<void>((resolve, reject) => {
log.once('log', (m: {message: string}) => {
assert.strictEqual(/Error: with a stack trace/.test(m.message), true);
assert.strictEqual(/at Test/.test(m.message), true);
resolve();
});
const err = new Error('with a stack trace');
log.log('verbose', 'oops', err);
setTimeout(() => reject(new Error('timeout')), 1000);
});
});
it('replaces sensitive messages', async function () {
log.updateAsyncStorage({isSensitive: true}, true);
log.log('verbose', 'test', markSensitive('log 1'));
assert.strictEqual(log.record.at(-1)!.message, '**SECURE**');
log.updateAsyncStorage({isSensitive: false}, true);
log.log('verbose', 'test', markSensitive('log 1'));
assert.strictEqual(log.record.at(-1)!.message, 'log 1');
});
it('max record size', function () {
log.maxRecordSize = 3;
log.log('verbose', 'test', 'log 1');
log.log('verbose', 'test', 'log 2');
log.log('verbose', 'test', 'log 3');
log.log('verbose', 'test', 'log 4');
assert.deepStrictEqual(
log.record.map(({message}) => message),
['log 2', 'log 3', 'log 4'],
);
log.maxRecordSize = 2;
log.log('verbose', 'test', 'log 5');
assert.deepStrictEqual(
log.record.map(({message}) => message),
['log 4', 'log 5'],
);
log.maxRecordSize = 3;
log.log('verbose', 'test', 'log 6');
assert.deepStrictEqual(
log.record.map(({message}) => message),
['log 4', 'log 5', 'log 6'],
);
});
});
describe('stream', function () {
beforeEach(function () {
log = new Log();
});
it('write with no stream', function () {
log.stream = null as any;
(log as any).write('message');
});
});
describe('emitLog', function () {
beforeEach(function () {
log = new Log();
});
it('to nonexistent level', function () {
(log as any).emitLog({prefix: 'test', level: 'asdf'});
});
});
describe('format', function () {
beforeEach(function () {
log = new Log();
});
it('with nonexistent stream', function () {
log.stream = null as any;
assert.strictEqual((log as any)._format('message'), undefined);
});
it('fg', function () {
log.enableColor();
const o = (log as any)._format('test message', {bg: 'blue'});
assert.ok(o.includes('\u001b[44mtest message\u001b[0m'));
});
it('bg', function () {
log.enableColor();
const o = (log as any)._format('test message', {bg: 'white'});
assert.ok(o.includes('\u001b[47mtest message\u001b[0m'));
});
it('bold', function () {
log.enableColor();
const o = (log as any)._format('test message', {bold: true});
assert.ok(o.includes('\u001b[1mtest message\u001b[0m'));
});
it('underline', function () {
log.enableColor();
const o = (log as any)._format('test message', {underline: true});
assert.ok(o.includes('\u001b[4mtest message\u001b[0m'));
});
it('inverse', function () {
log.enableColor();
const o = (log as any)._format('test message', {inverse: true});
assert.ok(o.includes('\u001b[7mtest message\u001b[0m'));
});
});
});