nodejs/deps/npm/test/lib/utils/log-file.js
npm CLI robot 063afa85fe
deps: upgrade npm to 10.8.1
PR-URL: https://github.com/nodejs/node/pull/53207
Reviewed-By: Richard Lau <rlau@redhat.com>
Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com>
Reviewed-By: Mohammed Keyvanzadeh <mohammadkeyvanzade94@gmail.com>
2024-05-30 11:21:05 +00:00

362 lines
9.1 KiB
JavaScript

const t = require('tap')
const _fs = require('node:fs')
const fs = _fs.promises
const path = require('node:path')
const os = require('node:os')
const fsMiniPass = require('fs-minipass')
const tmock = require('../../fixtures/tmock')
const LogFile = require('../../../lib/utils/log-file.js')
const { cleanCwd, cleanDate } = require('../../fixtures/clean-snapshot')
t.cleanSnapshot = (s) => cleanDate(cleanCwd(s))
const getId = (d = new Date()) => d.toISOString().replace(/[.:]/g, '_')
const last = arr => arr[arr.length - 1]
const range = (n) => Array.from(Array(n).keys())
const makeOldLogs = (count, oldStyle) => {
const d = new Date()
d.setHours(-1)
d.setSeconds(0)
return range(oldStyle ? count : (count / 2)).reduce((acc, i) => {
const cloneDate = new Date(d.getTime())
cloneDate.setSeconds(i)
const dateId = getId(cloneDate)
if (oldStyle) {
acc[`${dateId}-debug.log`] = 'hello'
} else {
acc[`${dateId}-debug-0.log`] = 'hello'
acc[`${dateId}-debug-1.log`] = 'hello'
}
return acc
}, {})
}
const cleanErr = (message) => {
const err = new Error(message)
const stack = err.stack.split('\n')
err.stack = stack[0] + '\n' + range(10)
.map((__, i) => stack[1].replace(/^(\s+at\s).*/, `$1stack trace line ${i}`))
.join('\n')
return err
}
const loadLogFile = async (t, { buffer = [], mocks, testdir = {}, ...options } = {}) => {
const root = t.testdir(testdir)
const MockLogFile = tmock(t, '{LIB}/utils/log-file.js', mocks)
const logFile = new MockLogFile(Object.keys(options).length ? options : undefined)
// Create a fake public method since there is not one on logFile anymore
logFile.log = (...b) => process.emit('log', ...b)
buffer.forEach((b) => logFile.log(...b))
const id = getId()
await logFile.load({ path: path.join(root, `${id}-`), ...options })
t.teardown(() => logFile.off())
return {
root,
logFile,
LogFile,
readLogs: async () => {
const logDir = await fs.readdir(root, { withFileTypes: true })
const logFiles = logDir
.filter(f => f.isFile())
.map((f) => path.join(root, f.name))
.filter((f) => _fs.existsSync(f))
return Promise.all(logFiles.map(async (f) => {
const content = await fs.readFile(f, 'utf8')
const rawLogs = content.split(os.EOL)
return {
filename: f,
content,
rawLogs,
logs: rawLogs.filter(Boolean),
}
}))
},
}
}
t.test('init', async t => {
const maxLogsPerFile = 10
const { root, logFile, readLogs } = await loadLogFile(t, {
maxLogsPerFile,
maxFilesPerProcess: 20,
buffer: [['error', 'buffered']],
})
for (const i of range(50)) {
logFile.log('error', `log ${i}`)
}
// Ignored
logFile.log('pause')
logFile.log('resume')
logFile.log('pause')
for (const i of range(50)) {
logFile.log('verb', `log ${i}`)
}
logFile.off()
logFile.log('error', 'ignored')
const logs = await readLogs()
t.equal(logs.length, 11, 'total log files')
t.ok(logs.slice(0, 10).every(f => f.logs.length === maxLogsPerFile), 'max logs per file')
t.ok(last(logs).logs.length, 1, 'last file has remaining logs')
t.ok(logs.every(f => last(f.rawLogs) === ''), 'all logs end with newline')
t.strictSame(
logFile.files,
logs.map((l) => path.resolve(root, l.filename))
)
})
t.test('max files per process', async t => {
const maxLogsPerFile = 10
const maxFilesPerProcess = 5
const { logFile, readLogs } = await loadLogFile(t, {
maxLogsPerFile,
maxFilesPerProcess,
})
for (const i of range(maxLogsPerFile * maxFilesPerProcess)) {
logFile.log('error', `log ${i}`)
}
for (const i of range(5)) {
logFile.log('verbose', `ignored after maxlogs hit ${i}`)
}
const logs = await readLogs()
t.equal(logs.length, maxFilesPerProcess, 'total log files')
t.match(last(last(logs).logs), /49 error log \d+/)
})
t.test('stream error', async t => {
let times = 0
const { logFile, readLogs } = await loadLogFile(t, {
maxLogsPerFile: 1,
maxFilesPerProcess: 99,
mocks: {
'fs-minipass': {
WriteStreamSync: class {
constructor (...args) {
if (times >= 5) {
throw new Error('bad stream')
}
times++
return new fsMiniPass.WriteStreamSync(...args)
}
},
},
},
})
for (const i of range(10)) {
logFile.log('verbose', `log ${i}`)
}
const logs = await readLogs()
t.equal(logs.length, 5, 'total log files')
})
t.test('initial stream error', async t => {
const { logFile, readLogs } = await loadLogFile(t, {
mocks: {
'fs-minipass': {
WriteStreamSync: class {
constructor () {
throw new Error('no stream')
}
},
},
},
})
for (const i of range(10)) {
logFile.log('verbose', `log ${i}`)
}
const logs = await readLogs()
t.equal(logs.length, 0, 'total log files')
})
t.test('turns off', async t => {
const { logFile, readLogs } = await loadLogFile(t)
logFile.log('error', 'test')
logFile.off()
logFile.log('error', 'test2')
logFile.load()
const logs = await readLogs()
t.match(last(last(logs).logs), /^\d+ error test$/)
})
t.test('cleans logs', async t => {
const logsMax = 5
const { readLogs } = await loadLogFile(t, {
logsMax,
testdir: makeOldLogs(10),
})
const logs = await readLogs()
t.equal(logs.length, logsMax + 1)
})
t.test('cleans logs even when find folder inside logs folder', async t => {
const logsMax = 5
const { readLogs } = await loadLogFile(t, {
logsMax,
testdir: {
...makeOldLogs(10),
ignore_folder: {
'ignored-file.txt': 'hello',
},
},
})
const logs = await readLogs()
t.equal(logs.length, logsMax + 1)
})
t.test('doesnt clean current log by default', async t => {
const logsMax = 1
const { readLogs, logFile } = await loadLogFile(t, {
logsMax,
testdir: makeOldLogs(10),
})
logFile.log('error', 'test')
const logs = await readLogs()
t.match(last(logs).content, /\d+ error test/)
})
t.test('negative logs max', async t => {
const logsMax = -10
const { readLogs, logFile } = await loadLogFile(t, {
logsMax,
testdir: makeOldLogs(10),
})
logFile.log('error', 'test')
const logs = await readLogs()
t.equal(logs.length, 0)
})
t.test('doesnt need to clean', async t => {
const logsMax = 20
const oldLogs = 10
const { readLogs } = await loadLogFile(t, {
logsMax,
testdir: makeOldLogs(oldLogs),
})
const logs = await readLogs()
t.equal(logs.length, oldLogs + 1)
})
t.test('cleans old style logs too', async t => {
const logsMax = 5
const oldLogs = 10
const { readLogs } = await loadLogFile(t, {
logsMax,
testdir: makeOldLogs(oldLogs, true),
})
const logs = await readLogs()
t.equal(logs.length, logsMax + 1)
})
t.test('rimraf error', async t => {
const logsMax = 5
const oldLogs = 10
let count = 0
const { readLogs } = await loadLogFile(t, {
logsMax,
testdir: makeOldLogs(oldLogs),
mocks: {
'node:fs/promises': {
readdir: fs.readdir,
rm: async (...args) => {
if (count >= 3) {
throw new Error('bad rimraf')
}
count++
return fs.rm(...args)
},
},
},
})
const logs = await readLogs()
t.equal(logs.length, oldLogs - 3 + 1)
t.match(last(logs).content, /error removing log file .* bad rimraf/)
})
t.test('delete log file while open', async t => {
const { logFile, root, readLogs } = await loadLogFile(t)
logFile.log('error', '', 'log 1')
const [log] = await readLogs(true)
t.match(log.content, /\d+ error log 1/)
await fs.unlink(path.resolve(root, log.filename))
logFile.log('error', '', 'log 2')
const logs = await readLogs()
// XXX: do some retry logic after error?
t.strictSame(logs, [], 'logs arent written after error')
})
t.test('snapshot', async t => {
const { logFile, readLogs } = await loadLogFile(t, { logsMax: 10 })
logFile.log('error', '', 'no prefix')
logFile.log('error', 'prefix', 'with prefix')
logFile.log('error', 'prefix', 1, 2, 3)
const nestedObj = { obj: { with: { many: { props: 1 } } } }
logFile.log('verbose', '', nestedObj)
logFile.log('verbose', '', JSON.stringify(nestedObj))
logFile.log('verbose', '', JSON.stringify(nestedObj, null, 2))
const arr = ['test', 'with', 'an', 'array']
logFile.log('verbose', '', arr)
logFile.log('verbose', '', JSON.stringify(arr))
logFile.log('verbose', '', JSON.stringify(arr, null, 2))
const nestedArr = ['test', ['with', ['an', ['array']]]]
logFile.log('verbose', '', nestedArr)
logFile.log('verbose', '', JSON.stringify(nestedArr))
logFile.log('verbose', '', JSON.stringify(nestedArr, null, 2))
// XXX: multiple errors are hard to parse visually
// the second error should start on a newline
logFile.log(...[
'error',
'pre',
'has',
'many',
'errors',
cleanErr('message'),
cleanErr('message2'),
])
const err = new Error('message')
delete err.stack
logFile.log(...[
'error',
'nostack',
err,
])
const logs = await readLogs()
t.matchSnapshot(logs.map(l => l.content).join('\n'))
})