handling files when stages and dirty at once
This commit is contained in:
151
test/logger.test.ts
Normal file
151
test/logger.test.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import { describe, expect, it, beforeEach, afterEach } from 'vitest'
|
||||
import { mkdtempSync, readFileSync, rmSync, existsSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { _resetLogger, formatErr, formatLine, getLogPath, initLogger, log, logger } from '../src/main/logger'
|
||||
|
||||
let dir: string
|
||||
|
||||
beforeEach(() => {
|
||||
dir = mkdtempSync(join(tmpdir(), 'helder-log-'))
|
||||
_resetLogger()
|
||||
})
|
||||
afterEach(() => {
|
||||
_resetLogger()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
function read(): string {
|
||||
return readFileSync(join(dir, 'helder.log'), 'utf8')
|
||||
}
|
||||
|
||||
describe('formatLine', () => {
|
||||
it('lays out ts, padded level, pid, scope and message', () => {
|
||||
const line = formatLine('info', 'git', 'loaded', undefined, 42, '2026-07-15T10:00:00.000Z')
|
||||
expect(line).toBe('2026-07-15T10:00:00.000Z INFO 42 git loaded\n')
|
||||
})
|
||||
|
||||
it('appends context as JSON', () => {
|
||||
const line = formatLine('warn', 'ipc', 'slow', { ms: 1200 }, 7, '2026-07-15T10:00:00.000Z')
|
||||
expect(line).toContain('{"ms":1200}')
|
||||
expect(line.endsWith('\n')).toBe(true)
|
||||
})
|
||||
|
||||
it('indents a multi-line message so a continuation never looks like a new entry', () => {
|
||||
// Electron's own console warnings arrive with embedded newlines.
|
||||
const line = formatLine('warn', 'console', 'line one\nline two', undefined, 1, 'T')
|
||||
expect(line).toBe('T WARN 1 console line one\n line two\n')
|
||||
// Every line after the first is indented → an entry always starts at col 0.
|
||||
for (const l of line.trimEnd().split('\n').slice(1)) expect(l.startsWith(' ')).toBe(true)
|
||||
})
|
||||
|
||||
it('indents multi-line context too', () => {
|
||||
const line = formatLine('error', 'x', 'boom', { stack: 'a\nb' }, 1, 'T')
|
||||
// JSON escapes the \n inside the string value, so this stays a single line.
|
||||
expect(line.split('\n').filter(Boolean)).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatErr', () => {
|
||||
it('keeps message and stack', () => {
|
||||
const e = new Error('nope')
|
||||
const out = formatErr(e)
|
||||
expect(out.message).toBe('nope')
|
||||
expect(out.stack).toContain('nope')
|
||||
})
|
||||
|
||||
it('follows cause — the line that usually explains the failure', () => {
|
||||
const root = new Error('EACCES')
|
||||
const e = new Error('save failed', { cause: root })
|
||||
expect(formatErr(e).cause).toContain('EACCES')
|
||||
})
|
||||
|
||||
it('survives a thrown string', () => {
|
||||
expect(formatErr('just a string').message).toBe('just a string')
|
||||
})
|
||||
|
||||
it('survives a thrown non-Error object', () => {
|
||||
expect(formatErr({ code: 7 }).message).toBe('{"code":7}')
|
||||
})
|
||||
|
||||
it('does not throw on circular values', () => {
|
||||
const a: Record<string, unknown> = {}
|
||||
a.self = a
|
||||
expect(() => formatErr(a)).not.toThrow()
|
||||
expect(formatErr(a).message).toContain('Circular')
|
||||
})
|
||||
})
|
||||
|
||||
describe('log', () => {
|
||||
it('creates the file and appends lines', () => {
|
||||
initLogger({ dir })
|
||||
logger.info('boot', 'hello')
|
||||
logger.warn('boot', 'careful')
|
||||
const out = read()
|
||||
expect(out).toContain('INFO')
|
||||
expect(out).toContain('hello')
|
||||
expect(out).toContain('WARN')
|
||||
expect(out.trim().split('\n')).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('creates a missing directory', () => {
|
||||
const nested = join(dir, 'a', 'b')
|
||||
initLogger({ dir: nested })
|
||||
logger.info('x', 'y')
|
||||
expect(existsSync(join(nested, 'helder.log'))).toBe(true)
|
||||
})
|
||||
|
||||
it('records the error with its stack', () => {
|
||||
initLogger({ dir })
|
||||
logger.error('save', 'write failed', new Error('EACCES: permission denied'), { path: 'a.ts' })
|
||||
const out = read()
|
||||
expect(out).toContain('EACCES: permission denied')
|
||||
expect(out).toContain('"path":"a.ts"')
|
||||
expect(out).toContain('"stack"')
|
||||
})
|
||||
|
||||
it('honours the level floor', () => {
|
||||
initLogger({ dir, level: 'warn' })
|
||||
logger.debug('x', 'debug line')
|
||||
logger.info('x', 'info line')
|
||||
logger.error('x', 'error line')
|
||||
const out = read()
|
||||
expect(out).not.toContain('debug line')
|
||||
expect(out).not.toContain('info line')
|
||||
expect(out).toContain('error line')
|
||||
})
|
||||
|
||||
it('is a no-op before initLogger rather than throwing', () => {
|
||||
expect(() => logger.info('x', 'y')).not.toThrow()
|
||||
expect(getLogPath()).toBeNull()
|
||||
})
|
||||
|
||||
it('exposes the active path', () => {
|
||||
initLogger({ dir })
|
||||
expect(getLogPath()).toBe(join(dir, 'helder.log'))
|
||||
})
|
||||
|
||||
it('never throws even when the log path is unwritable', () => {
|
||||
// Point at a path whose parent is a FILE: every append must fail.
|
||||
const wall = join(dir, 'wall')
|
||||
writeFileSync(wall, 'x')
|
||||
initLogger({ dir: join(wall, 'sub') })
|
||||
expect(() => logger.error('x', 'still fine')).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('rotation', () => {
|
||||
it('rolls the file once it passes the size cap and keeps writing', () => {
|
||||
initLogger({ dir })
|
||||
const big = 'x'.repeat(4000)
|
||||
// 2MB cap / ~4KB per line → ~525 lines to trip it. 700 is comfortably past.
|
||||
for (let i = 0; i < 700; i++) log('info', 'bulk', big)
|
||||
|
||||
expect(existsSync(join(dir, 'helder.1.log'))).toBe(true)
|
||||
// The live file is the post-rotation one and is still being appended to.
|
||||
logger.info('after', 'still logging')
|
||||
expect(read()).toContain('still logging')
|
||||
// And it's small again — proof the rotation actually moved the bytes.
|
||||
expect(read().length).toBeLessThan(4000 * 700)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user