The fileuploader.log rotation introduced in 3.3.2 lived inline in main.js — fine for the runtime path, but it required electron's `app` to even reach the function under test. Pull the rotation logic into lib/log-rotation.js (pure fs/path, no electron deps) and cover it properly: - ENOENT (file missing) → no-op - Below cap → no-op - Over cap → live → .1, returns true - Existing backups shift up: .1 → .2, .2 → .3 - At maxBackups limit → oldest dropped, others shift, live becomes .1 - Idempotent: rotating twice keeps the chain consistent - maxBackups=1: never grows past .1 - Invalid maxBytes (0/negative/NaN) → safe no-op - Provided debug callback receives a "rotated" message - File without extension still rotates correctly main.js now imports `maybeRotateLogFile` and calls it directly. 97/97 tests pass.
135 lines
5.1 KiB
JavaScript
135 lines
5.1 KiB
JavaScript
const { test, beforeEach, afterEach } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const os = require('os');
|
|
|
|
const { maybeRotateLogFile } = require('../lib/log-rotation');
|
|
|
|
let tmpDir;
|
|
let logFile;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mhu-log-rotation-'));
|
|
logFile = path.join(tmpDir, 'fileuploader.log');
|
|
});
|
|
|
|
afterEach(() => {
|
|
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
|
});
|
|
|
|
function writeBytes(p, n, fill = 'a') {
|
|
fs.writeFileSync(p, fill.repeat(n), 'utf-8');
|
|
}
|
|
|
|
test('returns false and skips rotation when file does not exist', () => {
|
|
const result = maybeRotateLogFile(logFile, 100);
|
|
assert.equal(result, false);
|
|
assert.equal(fs.existsSync(logFile), false);
|
|
});
|
|
|
|
test('returns false when file is below the size cap', () => {
|
|
writeBytes(logFile, 50);
|
|
const result = maybeRotateLogFile(logFile, 100);
|
|
assert.equal(result, false);
|
|
assert.equal(fs.statSync(logFile).size, 50, 'live file untouched');
|
|
assert.equal(fs.existsSync(logFile + '.1'), false, 'no .1 created');
|
|
});
|
|
|
|
test('rotates live file to .1 when over cap', () => {
|
|
writeBytes(logFile, 200, 'X');
|
|
const result = maybeRotateLogFile(logFile, 100, 3);
|
|
assert.equal(result, true);
|
|
assert.equal(fs.existsSync(logFile), false, 'live file moved away');
|
|
const expectedBackup = path.join(tmpDir, 'fileuploader.1.log');
|
|
assert.equal(fs.existsSync(expectedBackup), true, '.1 backup exists');
|
|
assert.equal(fs.statSync(expectedBackup).size, 200);
|
|
});
|
|
|
|
test('shifts existing backups up: .1 → .2, .2 → .3 on rotation', () => {
|
|
writeBytes(path.join(tmpDir, 'fileuploader.2.log'), 10, 'B');
|
|
writeBytes(path.join(tmpDir, 'fileuploader.1.log'), 20, 'A');
|
|
writeBytes(logFile, 200, 'L');
|
|
|
|
const result = maybeRotateLogFile(logFile, 100, 3);
|
|
assert.equal(result, true);
|
|
|
|
// Live file → .1 (latest live data)
|
|
assert.equal(fs.statSync(path.join(tmpDir, 'fileuploader.1.log')).size, 200);
|
|
// Old .1 → .2
|
|
assert.equal(fs.statSync(path.join(tmpDir, 'fileuploader.2.log')).size, 20);
|
|
// Old .2 → .3
|
|
assert.equal(fs.statSync(path.join(tmpDir, 'fileuploader.3.log')).size, 10);
|
|
});
|
|
|
|
test('drops oldest backup when at maxBackups limit', () => {
|
|
// Pre-populate all three backup slots.
|
|
writeBytes(path.join(tmpDir, 'fileuploader.3.log'), 5, 'C'); // oldest, will be dropped
|
|
writeBytes(path.join(tmpDir, 'fileuploader.2.log'), 10, 'B');
|
|
writeBytes(path.join(tmpDir, 'fileuploader.1.log'), 20, 'A');
|
|
writeBytes(logFile, 200, 'L');
|
|
|
|
const result = maybeRotateLogFile(logFile, 100, 3);
|
|
assert.equal(result, true);
|
|
|
|
// Old .3 (5 bytes 'C') gone, replaced by old .2.
|
|
const f3 = fs.statSync(path.join(tmpDir, 'fileuploader.3.log'));
|
|
assert.equal(f3.size, 10, 'old .2 became new .3 (the C-file was dropped)');
|
|
// .2 = old .1
|
|
assert.equal(fs.statSync(path.join(tmpDir, 'fileuploader.2.log')).size, 20);
|
|
// .1 = the live file we just rotated
|
|
assert.equal(fs.statSync(path.join(tmpDir, 'fileuploader.1.log')).size, 200);
|
|
});
|
|
|
|
test('is idempotent — second call on still-large file rotates again', () => {
|
|
writeBytes(logFile, 200, 'X');
|
|
maybeRotateLogFile(logFile, 100, 3);
|
|
// Simulate fresh writes after the first rotation
|
|
writeBytes(logFile, 200, 'Y');
|
|
const result = maybeRotateLogFile(logFile, 100, 3);
|
|
assert.equal(result, true);
|
|
// The .Y file is now .1, the .X file moved to .2
|
|
assert.equal(fs.readFileSync(path.join(tmpDir, 'fileuploader.1.log'), 'utf-8')[0], 'Y');
|
|
assert.equal(fs.readFileSync(path.join(tmpDir, 'fileuploader.2.log'), 'utf-8')[0], 'X');
|
|
});
|
|
|
|
test('maxBackups=1: only keeps a single .1 backup, never .2', () => {
|
|
writeBytes(logFile, 200, 'L');
|
|
maybeRotateLogFile(logFile, 100, 1);
|
|
writeBytes(logFile, 200, 'M');
|
|
maybeRotateLogFile(logFile, 100, 1);
|
|
|
|
// .1 holds the latest rotated content (M)
|
|
assert.equal(fs.readFileSync(path.join(tmpDir, 'fileuploader.1.log'), 'utf-8')[0], 'M');
|
|
// .2 must NOT exist
|
|
assert.equal(fs.existsSync(path.join(tmpDir, 'fileuploader.2.log')), false);
|
|
});
|
|
|
|
test('invalid maxBytes (0, negative, NaN) is a no-op', () => {
|
|
writeBytes(logFile, 1000, 'X');
|
|
for (const max of [0, -1, NaN]) {
|
|
const r = maybeRotateLogFile(logFile, max);
|
|
assert.equal(r, false, `maxBytes=${max} should be no-op`);
|
|
}
|
|
assert.equal(fs.existsSync(logFile), true);
|
|
assert.equal(fs.existsSync(logFile + '.1'), false);
|
|
});
|
|
|
|
test('logs through provided debug callback on rotation', () => {
|
|
writeBytes(logFile, 200, 'X');
|
|
const messages = [];
|
|
maybeRotateLogFile(logFile, 100, 3, (m) => messages.push(m));
|
|
assert.ok(messages.length >= 1, 'at least one log message');
|
|
assert.ok(messages.some(m => m.includes('rotated')), `expected "rotated" in: ${messages.join(' | ')}`);
|
|
});
|
|
|
|
test('handles file without extension correctly', () => {
|
|
const noExtFile = path.join(tmpDir, 'plainlog');
|
|
writeBytes(noExtFile, 200, 'P');
|
|
const result = maybeRotateLogFile(noExtFile, 100, 3);
|
|
assert.equal(result, true);
|
|
// base = the full path, ext = '', so backup name is "plainlog.1"
|
|
assert.equal(fs.existsSync(path.join(tmpDir, 'plainlog.1')), true);
|
|
assert.equal(fs.existsSync(noExtFile), false);
|
|
});
|