1const t = require('tap') 2const log = require('../../../lib/utils/log-shim') 3const mockLogs = require('../../fixtures/mock-logs') 4const mockGlobals = require('@npmcli/mock-globals') 5const tmock = require('../../fixtures/tmock') 6const util = require('util') 7 8const mockDisplay = (t, mocks) => { 9 const { logs, logMocks } = mockLogs(mocks) 10 const Display = tmock(t, '{LIB}/utils/display', { 11 ...mocks, 12 ...logMocks, 13 }) 14 const display = new Display() 15 t.teardown(() => display.off()) 16 return { display, logs } 17} 18 19t.test('setup', async (t) => { 20 const { display } = mockDisplay(t) 21 22 display.load({ timing: true, loglevel: 'notice' }) 23 t.equal(log.level, 'notice') 24 25 display.load({ timing: false, loglevel: 'notice' }) 26 t.equal(log.level, 'notice') 27 28 display.load({ color: true }) 29 t.equal(log.useColor(), true) 30 31 display.load({ unicode: true }) 32 t.equal(log.gauge._theme.hasUnicode, true) 33 34 display.load({ unicode: false }) 35 t.equal(log.gauge._theme.hasUnicode, false) 36 37 mockGlobals(t, { 'process.stderr.isTTY': true }) 38 display.load({ progress: true }) 39 t.equal(log.progressEnabled, true) 40}) 41 42t.test('can log cleanly', async (t) => { 43 const explains = [] 44 const { display, logs } = mockDisplay(t, { 45 npmlog: { 46 error: (...args) => logs.push(['error', ...args]), 47 warn: (...args) => logs.push(['warn', ...args]), 48 }, 49 '{LIB}/utils/explain-eresolve.js': { 50 explain: (...args) => { 51 explains.push(args) 52 return 'explanation' 53 }, 54 }, 55 }) 56 57 display.log('error', 'test\x00message') 58 t.match(logs.error, [['test^@message']]) 59 60 display.log('warn', 'ERESOLVE', 'hello', { some: 'object' }) 61 t.match(logs.warn, [['ERESOLVE', 'hello']]) 62 t.match(explains, [[{ some: 'object' }, null, 2]]) 63}) 64 65t.test('handles log throwing', async (t) => { 66 const errors = [] 67 mockGlobals(t, { 68 'console.error': (...args) => errors.push(args), 69 }) 70 const { display } = mockDisplay(t, { 71 npmlog: { 72 verbose: () => { 73 throw new Error('verbose') 74 }, 75 }, 76 '{LIB}/utils/explain-eresolve.js': { 77 explain: () => { 78 throw new Error('explain') 79 }, 80 }, 81 }) 82 83 display.log('warn', 'ERESOLVE', 'hello', { some: 'object' }) 84 t.match(errors, [ 85 [/attempt to log .* crashed/, Error('explain'), Error('verbose')], 86 ]) 87}) 88 89class CustomObj { 90 [util.inspect.custom] () { 91 return this.inspected 92 } 93} 94 95t.test('Display.clean', async (t) => { 96 const Display = require('../../../lib/utils/display') 97 const customNaN = new CustomObj() 98 const customNull = new CustomObj() 99 const customNumber = new CustomObj() 100 const customObject = new CustomObj() 101 const customString = new CustomObj() 102 const customUndefined = new CustomObj() 103 customNaN.inspected = NaN 104 customNull.inspected = null 105 customNumber.inspected = 477 106 customObject.inspected = { custom: 'rend\x00ering' } 107 customString.inspected = 'custom\x00rendering' 108 customUndefined.inspected = undefined 109 t.test('strings', async (t) => { 110 const tests = [ 111 [477, '477'], 112 [null, 'null'], 113 [NaN, 'NaN'], 114 [true, 'true'], 115 [undefined, 'undefined'], 116 ['', ''], 117 // Cover the bounds of each range and a few characters from inside each range 118 // \x00 through \x1f 119 ['hello\x00world', 'hello^@world'], 120 ['hello\x07world', 'hello^Gworld'], 121 ['hello\x1bworld', 'hello^[world'], 122 ['hello\x1eworld', 'hello^^world'], 123 ['hello\x1fworld', 'hello^_world'], 124 // \x7f is C0 125 ['hello\x7fworld', 'hello^?world'], 126 // \x80 through \x9f 127 ['hello\x80world', 'hello^@world'], 128 ['hello\x87world', 'hello^Gworld'], 129 ['hello\x9eworld', 'hello^^world'], 130 ['hello\x9fworld', 'hello^_world'], 131 // Allowed C0 132 ['hello\tworld', 'hello\tworld'], 133 ['hello\nworld', 'hello\nworld'], 134 ['hello\vworld', 'hello\vworld'], 135 ['hello\rworld', 'hello\rworld'], 136 // Allowed SGR 137 ['hello\x1b[38;5;254mworld', 'hello\x1b[38;5;254mworld'], 138 ['hello\x1b[mworld', 'hello\x1b[mworld'], 139 // Unallowed CSI / OSC 140 ['hello\x1b[2Aworld', 'hello^[[2Aworld'], 141 ['hello\x9b[2Aworld', 'hello^[[2Aworld'], 142 ['hello\x9decho goodbye\x9cworld', 'hello^]echo goodbye^\\world'], 143 // This is done twice to ensure we define inspect.custom as writable 144 [{ test: 'object' }, "{ test: 'object' }"], 145 [{ test: 'object' }, "{ test: 'object' }"], 146 // Make sure custom util.inspect doesn't bypass our cleaning 147 [customNaN, 'NaN'], 148 [customNull, 'null'], 149 [customNumber, '477'], 150 [customObject, "{ custom: 'rend\\x00ering' }"], 151 [customString, 'custom^@rendering'], 152 [customUndefined, 'undefined'], 153 // UTF-16 form of 8-bit C1 154 ['hello\xc2\x9bworld', 'hello\xc2^[world'], 155 ] 156 for (const [dirty, clean] of tests) { 157 const cleaned = Display.clean(dirty) 158 t.equal(util.format(cleaned), clean) 159 } 160 }) 161}) 162