Heartbeat

This commit is contained in:
chk
2026-06-12 17:03:38 +02:00
parent 4db6c472b7
commit 6fc6605080
11 changed files with 342 additions and 32 deletions

View File

@@ -0,0 +1,147 @@
'use strict';
// Heartbeat-Tests für TelnetSenderGRBL:
// Erkennung toter Verbindungen nach NotAus / Stromausfall beim Roboter.
const { EventEmitter } = require('events');
const TelnetSenderGRBL = require('../robot/TelnetSenderGRBL');
// ── Hilfsfunktionen ─────────────────────────────────────────────────────────
function makeRawSocket() {
const em = new EventEmitter();
return Object.assign(em, {
write: jest.fn(),
end: jest.fn(),
destroy: jest.fn(),
setKeepAlive: jest.fn(),
});
}
function makeTelnetSocket() {
const em = new EventEmitter();
return Object.assign(em, {
write: jest.fn(),
end: jest.fn(),
destroy: jest.fn(),
});
}
/**
* Erstellt einen Sender mit gemockten Abhängigkeiten und simuliert
* einen erfolgreichen Verbindungsaufbau.
*/
function setup({ heartbeatInterval = 1000, deadTimeout = 5000 } = {}) {
const rawSocket = makeRawSocket();
const telnetSock = makeTelnetSocket();
// Intervall-Verwaltung: injiziert, damit kein echter setInterval läuft.
const activeIntervals = new Map();
let nextId = 1;
const setIntervalFn = jest.fn((cb) => { const id = nextId++; activeIntervals.set(id, cb); return id; });
const clearIntervalFn = jest.fn((id) => { activeIntervals.delete(id); });
// Timeout-Mock: verhindert echte setTimeout-Aufrufe bei Reconnect-Scheduling.
const setTimeoutFn = jest.fn(() => 99);
const clearTimeoutFn = jest.fn();
const sender = new TelnetSenderGRBL(
'robot.local', 5000,
'x', 'y', 'z', null, null, null, null,
{
netModule: { createConnection: jest.fn(() => rawSocket) },
TelnetSocketClass: jest.fn(() => telnetSock),
setIntervalFn,
clearIntervalFn,
setTimeoutFn,
clearTimeoutFn,
heartbeatInterval,
deadTimeout,
autoConnect: false,
}
);
// Verbindungsaufbau simulieren
sender.connect();
rawSocket.emit('connect');
/** Heartbeat-Callback manuell auslösen. */
const fireHeartbeat = () => {
const entries = [...activeIntervals.values()];
if (entries.length > 0) entries[0]();
};
return { sender, rawSocket, telnetSock, activeIntervals, fireHeartbeat };
}
// ── Tests ────────────────────────────────────────────────────────────────────
describe('TelnetSenderGRBL — Heartbeat / NotAus-Erkennung', () => {
test('TCP-Keepalive wird beim Verbinden aktiviert', () => {
const { rawSocket } = setup();
expect(rawSocket.setKeepAlive).toHaveBeenCalledWith(true, 2000);
});
test('Heartbeat-Timer wird nach Verbindung gestartet', () => {
const { activeIntervals } = setup();
expect(activeIntervals.size).toBe(1);
});
test('Heartbeat sendet ? an den Controller', () => {
const { telnetSock, fireHeartbeat } = setup();
fireHeartbeat();
expect(telnetSock.write).toHaveBeenCalledWith('?');
});
test('Socket wird zerstört wenn deadTimeout überschritten', () => {
const { sender, rawSocket, fireHeartbeat } = setup({ deadTimeout: 5000 });
sender._lastDataAt = Date.now() - 20_000; // älter als deadTimeout → tot
fireHeartbeat();
expect(rawSocket.destroy).toHaveBeenCalled();
});
test('Heartbeat-Timer wird nach Timeout-Erkennung gestoppt', () => {
const { sender, activeIntervals, fireHeartbeat } = setup({ deadTimeout: 5000 });
sender._lastDataAt = Date.now() - 20_000;
fireHeartbeat();
expect(activeIntervals.size).toBe(0);
});
test('Kein Destroy wenn Daten innerhalb deadTimeout empfangen wurden', () => {
const { sender, rawSocket, telnetSock, fireHeartbeat } = setup({ deadTimeout: 5000 });
sender._lastDataAt = Date.now(); // soeben empfangen → kein Timeout
fireHeartbeat();
expect(rawSocket.destroy).not.toHaveBeenCalled();
expect(telnetSock.write).toHaveBeenCalledWith('?');
});
test('Eingehende Daten aktualisieren _lastDataAt', () => {
const { sender, rawSocket } = setup();
const before = sender._lastDataAt;
rawSocket.emit('data', Buffer.from('<Idle|MPos:0,0,0>'));
expect(sender._lastDataAt).toBeGreaterThanOrEqual(before);
});
test('Heartbeat-Timer wird bei socket close gestoppt', () => {
const { activeIntervals, telnetSock } = setup();
expect(activeIntervals.size).toBe(1);
telnetSock.emit('close');
expect(activeIntervals.size).toBe(0);
});
test('Heartbeat-Timer wird bei disconnect() gestoppt', () => {
const { sender, activeIntervals } = setup();
expect(activeIntervals.size).toBe(1);
sender.disconnect();
expect(activeIntervals.size).toBe(0);
});
test('Heartbeat stoppt sich selbst wenn tSocket null ist', () => {
const { sender, rawSocket, telnetSock, activeIntervals, fireHeartbeat } = setup();
sender.tSocket = null;
fireHeartbeat();
expect(rawSocket.destroy).not.toHaveBeenCalled();
expect(telnetSock.write).not.toHaveBeenCalled();
expect(activeIntervals.size).toBe(0);
});
});