Konfig in robot.json
This commit is contained in:
150
test/RobotConfig.test.js
Normal file
150
test/RobotConfig.test.js
Normal file
@@ -0,0 +1,150 @@
|
||||
'use strict';
|
||||
const { load, DEFAULTS } = require('../robot/RobotConfig');
|
||||
|
||||
function makeFs(content) {
|
||||
return {
|
||||
readFileSync: jest.fn(() => content)
|
||||
};
|
||||
}
|
||||
|
||||
function makeFailFs() {
|
||||
return {
|
||||
readFileSync: jest.fn(() => { throw new Error('ENOENT'); })
|
||||
};
|
||||
}
|
||||
|
||||
const log = { warn: jest.fn(), log: jest.fn(), error: jest.fn() };
|
||||
|
||||
const FULL_ROBOT_JSON = {
|
||||
kinematics: { type: 'arm3segmentlinearx' },
|
||||
motion: { defaultFeedrate: 2300, speedMode: 'legacy', speedModeOptions: ['legacy', 'correct'] },
|
||||
controllers: {
|
||||
base: { ip: 'fluidNcBase.local', port: 2300, protocol: 'telnet', axes: ['x', 'y', 'z'] },
|
||||
elbow: { ip: 'fluidNcEllbow.local', port: 5000, protocol: 'telnet', axes: ['a', null, null] },
|
||||
hand: { ip: 'fluidNcHand.local', port: 5000, protocol: 'telnet', axes: ['c', 'e', 'b'] }
|
||||
},
|
||||
links: {
|
||||
Arm1: { skeleton: { from: [0,0,0], to: [0,-250,0] } },
|
||||
Arm2: { skeleton: { from: [0,0,0], to: [0,-250,0] } },
|
||||
Ellbow: { skeleton: { from: [0,0,0], to: [90,0,0] } }
|
||||
}
|
||||
};
|
||||
|
||||
describe('RobotConfig.load — Vollständige robot.json', () => {
|
||||
let cfg;
|
||||
beforeEach(() => {
|
||||
cfg = load(makeFs(JSON.stringify(FULL_ROBOT_JSON)), {}, log);
|
||||
});
|
||||
|
||||
test('kinematics.type aus robot.json', () => {
|
||||
expect(cfg.kinematics.type).toBe('arm3segmentlinearx');
|
||||
});
|
||||
|
||||
test('l1/l2 aus links.Arm1/Arm2.skeleton.to[1] (Betrag)', () => {
|
||||
expect(cfg.kinematics.l1).toBe(250);
|
||||
expect(cfg.kinematics.l2).toBe(250);
|
||||
});
|
||||
|
||||
test('l3 aus links.Ellbow.skeleton.to[0]', () => {
|
||||
expect(cfg.kinematics.l3).toBe(90);
|
||||
});
|
||||
|
||||
test('motion.defaultFeedrate aus robot.json', () => {
|
||||
expect(cfg.motion.defaultFeedrate).toBe(2300);
|
||||
});
|
||||
|
||||
test('motion.speedMode aus robot.json', () => {
|
||||
expect(cfg.motion.speedMode).toBe('legacy');
|
||||
});
|
||||
|
||||
test('motion.useSpeedCalc false für legacy', () => {
|
||||
expect(cfg.motion.useSpeedCalc).toBe(false);
|
||||
});
|
||||
|
||||
test('controllers enthält alle drei Endpunkte', () => {
|
||||
expect(cfg.controllers.base.ip).toBe('fluidNcBase.local');
|
||||
expect(cfg.controllers.base.port).toBe(2300);
|
||||
expect(cfg.controllers.base.protocol).toBe('telnet');
|
||||
expect(cfg.controllers.elbow.port).toBe(5000);
|
||||
expect(cfg.controllers.hand.ip).toBe('fluidNcHand.local');
|
||||
});
|
||||
|
||||
test('axesByController gibt korrektes Array zurück', () => {
|
||||
expect(cfg.axesByController('base')).toEqual(['x', 'y', 'z']);
|
||||
expect(cfg.axesByController('elbow')).toEqual(['a', null, null]);
|
||||
expect(cfg.axesByController('hand')).toEqual(['c', 'e', 'b']);
|
||||
});
|
||||
|
||||
test('axesByController gibt [] für unbekannten Key zurück', () => {
|
||||
expect(cfg.axesByController('unknown')).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('RobotConfig.load — Fehlerbehandlung', () => {
|
||||
test('fehlende robot.json → Defaults', () => {
|
||||
const cfg = load(makeFailFs(), {}, log);
|
||||
expect(cfg.kinematics.l1).toBe(DEFAULTS.kinematics.l1);
|
||||
expect(cfg.motion.defaultFeedrate).toBe(DEFAULTS.motion.defaultFeedrate);
|
||||
expect(cfg.controllers.base.ip).toBe(DEFAULTS.controllers.base.ip);
|
||||
});
|
||||
|
||||
test('ungültiges JSON → Defaults', () => {
|
||||
const cfg = load(makeFs('{ not valid json }'), {}, log);
|
||||
expect(cfg.kinematics.type).toBe(DEFAULTS.kinematics.type);
|
||||
});
|
||||
|
||||
test('fehlende links → kinematics-Defaults', () => {
|
||||
const json = { ...FULL_ROBOT_JSON };
|
||||
delete json.links;
|
||||
const cfg = load(makeFs(JSON.stringify(json)), {}, log);
|
||||
expect(cfg.kinematics.l1).toBe(DEFAULTS.kinematics.l1);
|
||||
expect(cfg.kinematics.l3).toBe(DEFAULTS.kinematics.l3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('RobotConfig.load — Env-Override', () => {
|
||||
test('ROBOT_DEFAULT_FEEDRATE überschreibt robot.json', () => {
|
||||
const env = { ROBOT_DEFAULT_FEEDRATE: '5000' };
|
||||
const cfg = load(makeFs(JSON.stringify(FULL_ROBOT_JSON)), env, log);
|
||||
expect(cfg.motion.defaultFeedrate).toBe(5000);
|
||||
});
|
||||
|
||||
test('ROBOT_SPEED_MODE=correct setzt useSpeedCalc=true', () => {
|
||||
const env = { ROBOT_SPEED_MODE: 'correct' };
|
||||
const cfg = load(makeFs(JSON.stringify(FULL_ROBOT_JSON)), env, log);
|
||||
expect(cfg.motion.speedMode).toBe('correct');
|
||||
expect(cfg.motion.useSpeedCalc).toBe(true);
|
||||
});
|
||||
|
||||
test('GRBL_BASE_IP überschreibt controller.base.ip', () => {
|
||||
const env = { GRBL_BASE_IP: '192.168.1.10' };
|
||||
const cfg = load(makeFs(JSON.stringify(FULL_ROBOT_JSON)), env, log);
|
||||
expect(cfg.controllers.base.ip).toBe('192.168.1.10');
|
||||
});
|
||||
|
||||
test('GRBL_ELLBOW_IP überschreibt controller.elbow.ip', () => {
|
||||
const env = { GRBL_ELLBOW_IP: '192.168.1.11' };
|
||||
const cfg = load(makeFs(JSON.stringify(FULL_ROBOT_JSON)), env, log);
|
||||
expect(cfg.controllers.elbow.ip).toBe('192.168.1.11');
|
||||
});
|
||||
|
||||
test('GRBL_HAND_IP überschreibt controller.hand.ip', () => {
|
||||
const env = { GRBL_HAND_IP: '192.168.1.12' };
|
||||
const cfg = load(makeFs(JSON.stringify(FULL_ROBOT_JSON)), env, log);
|
||||
expect(cfg.controllers.hand.ip).toBe('192.168.1.12');
|
||||
});
|
||||
});
|
||||
|
||||
describe('RobotConfig.load — speedMode correct', () => {
|
||||
test('speedMode correct aus robot.json setzt useSpeedCalc=true', () => {
|
||||
const json = { ...FULL_ROBOT_JSON, motion: { ...FULL_ROBOT_JSON.motion, speedMode: 'correct' } };
|
||||
const cfg = load(makeFs(JSON.stringify(json)), {}, log);
|
||||
expect(cfg.motion.useSpeedCalc).toBe(true);
|
||||
});
|
||||
|
||||
test('ROBOT_USE_SPEED_CALC=true setzt useSpeedCalc', () => {
|
||||
const env = { ROBOT_USE_SPEED_CALC: 'true' };
|
||||
const cfg = load(makeFs(JSON.stringify(FULL_ROBOT_JSON)), env, log);
|
||||
expect(cfg.motion.useSpeedCalc).toBe(true);
|
||||
});
|
||||
});
|
||||
272
test/RobotConfigService.test.js
Normal file
272
test/RobotConfigService.test.js
Normal file
@@ -0,0 +1,272 @@
|
||||
const fs = require('fs');
|
||||
const https = require('https');
|
||||
const path = require('path');
|
||||
const express = require('express');
|
||||
|
||||
const service = require('../server/RobotConfigService');
|
||||
|
||||
const DATA_DIR = path.join(__dirname, '..', 'data', 'robot');
|
||||
const ROBOT_JSON = path.join(DATA_DIR, 'robot.json');
|
||||
const TEST_KEY = 'test-api-key-12345';
|
||||
|
||||
const SAMPLE_ROBOT = {
|
||||
links: {
|
||||
Arm1: { size: [70, 250, 70] },
|
||||
Arm2: { size: [70, 250, 70] },
|
||||
Ellbow: { size: [90, 0, 0] }
|
||||
}
|
||||
};
|
||||
|
||||
// ── HTTPS-Test-Server ─────────────────────────────────────────────────────────
|
||||
|
||||
function makeServer(apiKey) {
|
||||
const app = express();
|
||||
service.register(app, { apiKey });
|
||||
const key = fs.readFileSync('https/localhost.key');
|
||||
const cert = fs.readFileSync('https/localhost.pem');
|
||||
return https.createServer({ key, cert, passphrase: 'abcd' }, app);
|
||||
}
|
||||
|
||||
function listen(server) {
|
||||
return new Promise((resolve, reject) => {
|
||||
server.listen(0, () => {
|
||||
const addr = server.address();
|
||||
addr ? resolve(addr.port) : reject(new Error('no port'));
|
||||
});
|
||||
server.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
function closeServer(server) {
|
||||
return new Promise(resolve => { server.close(resolve); });
|
||||
}
|
||||
|
||||
function request(method, url, { body, token } = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const agent = new https.Agent({ rejectUnauthorized: false });
|
||||
const payload = body ? JSON.stringify(body) : undefined;
|
||||
const headers = {
|
||||
...(payload ? { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) } : {}),
|
||||
...(token ? { 'Authorization': `Bearer ${token}` } : {})
|
||||
};
|
||||
const req = https.request(url, { method, agent, headers }, (res) => {
|
||||
let data = '';
|
||||
res.on('data', chunk => { data += chunk; });
|
||||
res.on('end', () => resolve({ statusCode: res.statusCode, body: data }));
|
||||
});
|
||||
req.on('error', reject);
|
||||
if (payload) req.write(payload);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// ── Setup / Teardown ──────────────────────────────────────────────────────────
|
||||
|
||||
function cleanupTestFiles() {
|
||||
for (const f of fs.readdirSync(DATA_DIR)) {
|
||||
if (f === '.gitkeep' || f === '.apikey') continue;
|
||||
fs.unlinkSync(path.join(DATA_DIR, f));
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
fs.mkdirSync(DATA_DIR, { recursive: true });
|
||||
cleanupTestFiles();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanupTestFiles();
|
||||
});
|
||||
|
||||
// ── Einzel-Funktionen ─────────────────────────────────────────────────────────
|
||||
|
||||
describe('readRobotJson / writeRobotJson', () => {
|
||||
test('writeRobotJson schreibt Datei und legt Snapshot an', async () => {
|
||||
fs.writeFileSync(ROBOT_JSON, JSON.stringify({ version: 1 }), 'utf8');
|
||||
const { snapshotFile } = await service.writeRobotJson({ version: 2 });
|
||||
|
||||
expect(snapshotFile).toMatch(/^robot_\d{8}_\d{6}\.json$/);
|
||||
expect(fs.existsSync(path.join(DATA_DIR, snapshotFile))).toBe(true);
|
||||
|
||||
const written = JSON.parse(fs.readFileSync(ROBOT_JSON, 'utf8'));
|
||||
expect(written).toEqual({ version: 2 });
|
||||
|
||||
const snapshot = JSON.parse(fs.readFileSync(path.join(DATA_DIR, snapshotFile), 'utf8'));
|
||||
expect(snapshot).toEqual({ version: 1 });
|
||||
});
|
||||
|
||||
test('kein Snapshot wenn robot.json noch nicht existiert', async () => {
|
||||
const { snapshotFile } = await service.writeRobotJson({ fresh: true });
|
||||
expect(fs.existsSync(path.join(DATA_DIR, snapshotFile))).toBe(false);
|
||||
});
|
||||
|
||||
test('readRobotJson liest die aktuelle Datei', async () => {
|
||||
fs.writeFileSync(ROBOT_JSON, JSON.stringify(SAMPLE_ROBOT), 'utf8');
|
||||
const data = await service.readRobotJson();
|
||||
expect(data).toEqual(SAMPLE_ROBOT);
|
||||
});
|
||||
|
||||
test('readRobotJson wirft, wenn robot.json fehlt', async () => {
|
||||
await expect(service.readRobotJson()).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('listHistory / readSnapshot', () => {
|
||||
test('listHistory gibt leere Liste zurück wenn kein Snapshot', async () => {
|
||||
const list = await service.listHistory();
|
||||
expect(list).toEqual([]);
|
||||
});
|
||||
|
||||
test('listHistory listet vorhandene Snapshots, neueste zuerst', async () => {
|
||||
fs.writeFileSync(path.join(DATA_DIR, 'robot_20260101_120000.json'), '{}', 'utf8');
|
||||
fs.writeFileSync(path.join(DATA_DIR, 'robot_20260102_083000.json'), '{}', 'utf8');
|
||||
|
||||
const list = await service.listHistory();
|
||||
expect(list.map(e => e.filename)).toEqual([
|
||||
'robot_20260102_083000.json',
|
||||
'robot_20260101_120000.json'
|
||||
]);
|
||||
expect(list[0]).toEqual({ filename: 'robot_20260102_083000.json', day: '20260102', timestamp: '083000' });
|
||||
});
|
||||
|
||||
test('readSnapshot liest einen bestimmten Snapshot', async () => {
|
||||
const ts = '20260101_120000';
|
||||
fs.writeFileSync(path.join(DATA_DIR, `robot_${ts}.json`), JSON.stringify({ x: 42 }), 'utf8');
|
||||
const data = await service.readSnapshot(ts);
|
||||
expect(data).toEqual({ x: 42 });
|
||||
});
|
||||
|
||||
test('readSnapshot wirft bei unbekanntem Timestamp', async () => {
|
||||
await expect(service.readSnapshot('99999999_999999')).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Pruning', () => {
|
||||
test('≤ 100 Snapshots pro Tag bleiben alle erhalten', async () => {
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
fs.writeFileSync(path.join(DATA_DIR, `robot_2026061${i}_120000.json`), '{}', 'utf8');
|
||||
}
|
||||
fs.writeFileSync(ROBOT_JSON, '{}', 'utf8');
|
||||
await service.writeRobotJson({ pruned: true });
|
||||
|
||||
const list = await service.listHistory();
|
||||
// 5 bestehende (verschiedene Tage) + 1 neuer Snapshot = 6
|
||||
expect(list.length).toBe(6);
|
||||
});
|
||||
|
||||
test('> 100 Snapshots eines Tages: nur neuester bleibt', async () => {
|
||||
const day = '20260101';
|
||||
for (let i = 0; i < 102; i++) {
|
||||
const hh = String(Math.floor(i / 3600)).padStart(2, '0');
|
||||
const mm = String(Math.floor((i % 3600) / 60)).padStart(2, '0');
|
||||
const ss = String(i % 60).padStart(2, '0');
|
||||
fs.writeFileSync(path.join(DATA_DIR, `robot_${day}_${hh}${mm}${ss}.json`), `{"i":${i}}`, 'utf8');
|
||||
}
|
||||
// Neuester (lexikographisch letzter) vorher anlegen
|
||||
const newest = `robot_${day}_235959.json`;
|
||||
fs.writeFileSync(path.join(DATA_DIR, newest), '{"newest":true}', 'utf8');
|
||||
|
||||
fs.writeFileSync(ROBOT_JSON, '{}', 'utf8');
|
||||
await service.writeRobotJson({ trigger: true });
|
||||
|
||||
const list = await service.listHistory();
|
||||
const daySnapshots = list.filter(e => e.day === day);
|
||||
expect(daySnapshots.length).toBe(1);
|
||||
expect(daySnapshots[0].filename).toBe(newest);
|
||||
});
|
||||
});
|
||||
|
||||
// ── HTTP-Routen ───────────────────────────────────────────────────────────────
|
||||
|
||||
describe('HTTP GET /api/robot', () => {
|
||||
let server, port;
|
||||
|
||||
beforeEach(async () => { server = makeServer(TEST_KEY); port = await listen(server); });
|
||||
afterEach(async () => { await closeServer(server); });
|
||||
|
||||
test('404 wenn robot.json nicht existiert', async () => {
|
||||
const { statusCode } = await request('GET', `https://127.0.0.1:${port}/api/robot`);
|
||||
expect(statusCode).toBe(404);
|
||||
});
|
||||
|
||||
test('200 mit robot.json-Inhalt', async () => {
|
||||
fs.writeFileSync(ROBOT_JSON, JSON.stringify(SAMPLE_ROBOT), 'utf8');
|
||||
const { statusCode, body } = await request('GET', `https://127.0.0.1:${port}/api/robot`);
|
||||
expect(statusCode).toBe(200);
|
||||
expect(JSON.parse(body)).toEqual(SAMPLE_ROBOT);
|
||||
});
|
||||
});
|
||||
|
||||
describe('HTTP PUT /api/robot', () => {
|
||||
let server, port;
|
||||
|
||||
beforeEach(async () => { server = makeServer(TEST_KEY); port = await listen(server); });
|
||||
afterEach(async () => { await closeServer(server); });
|
||||
|
||||
test('401 ohne Authorization-Header', async () => {
|
||||
const { statusCode } = await request('PUT', `https://127.0.0.1:${port}/api/robot`, { body: SAMPLE_ROBOT });
|
||||
expect(statusCode).toBe(401);
|
||||
});
|
||||
|
||||
test('401 mit falschem Key', async () => {
|
||||
const { statusCode } = await request('PUT', `https://127.0.0.1:${port}/api/robot`, {
|
||||
body: SAMPLE_ROBOT, token: 'wrong-key'
|
||||
});
|
||||
expect(statusCode).toBe(401);
|
||||
});
|
||||
|
||||
test('200 mit korrektem Key — schreibt Datei', async () => {
|
||||
const { statusCode, body } = await request('PUT', `https://127.0.0.1:${port}/api/robot`, {
|
||||
body: SAMPLE_ROBOT, token: TEST_KEY
|
||||
});
|
||||
expect(statusCode).toBe(200);
|
||||
const result = JSON.parse(body);
|
||||
expect(result.ok).toBe(true);
|
||||
expect(result.snapshotFile).toMatch(/^robot_\d{8}_\d{6}\.json$/);
|
||||
|
||||
const written = JSON.parse(fs.readFileSync(ROBOT_JSON, 'utf8'));
|
||||
expect(written).toEqual(SAMPLE_ROBOT);
|
||||
});
|
||||
});
|
||||
|
||||
describe('HTTP GET /api/robot/history', () => {
|
||||
let server, port;
|
||||
|
||||
beforeEach(async () => { server = makeServer(TEST_KEY); port = await listen(server); });
|
||||
afterEach(async () => { await closeServer(server); });
|
||||
|
||||
test('200 mit leerer History', async () => {
|
||||
const { statusCode, body } = await request('GET', `https://127.0.0.1:${port}/api/robot/history`);
|
||||
expect(statusCode).toBe(200);
|
||||
expect(JSON.parse(body)).toEqual({ history: [] });
|
||||
});
|
||||
|
||||
test('200 listet vorhandene Snapshots', async () => {
|
||||
fs.writeFileSync(path.join(DATA_DIR, 'robot_20260611_100000.json'), '{}', 'utf8');
|
||||
const { statusCode, body } = await request('GET', `https://127.0.0.1:${port}/api/robot/history`);
|
||||
expect(statusCode).toBe(200);
|
||||
const { history } = JSON.parse(body);
|
||||
expect(history).toHaveLength(1);
|
||||
expect(history[0].filename).toBe('robot_20260611_100000.json');
|
||||
});
|
||||
});
|
||||
|
||||
describe('HTTP GET /api/robot/history/:ts', () => {
|
||||
let server, port;
|
||||
|
||||
beforeEach(async () => { server = makeServer(TEST_KEY); port = await listen(server); });
|
||||
afterEach(async () => { await closeServer(server); });
|
||||
|
||||
test('200 für vorhandenen Snapshot', async () => {
|
||||
const ts = '20260611_100000';
|
||||
fs.writeFileSync(path.join(DATA_DIR, `robot_${ts}.json`), JSON.stringify({ snap: true }), 'utf8');
|
||||
const { statusCode, body } = await request('GET', `https://127.0.0.1:${port}/api/robot/history/${ts}`);
|
||||
expect(statusCode).toBe(200);
|
||||
expect(JSON.parse(body)).toEqual({ snap: true });
|
||||
});
|
||||
|
||||
test('404 für unbekannten Snapshot', async () => {
|
||||
const { statusCode } = await request('GET', `https://127.0.0.1:${port}/api/robot/history/99991231_235959`);
|
||||
expect(statusCode).toBe(404);
|
||||
});
|
||||
});
|
||||
@@ -43,7 +43,7 @@ describe('startRobot orchestrator', () => {
|
||||
consoleObj: { log: jest.fn(), warn: jest.fn(), error: jest.fn() }
|
||||
});
|
||||
|
||||
expect(readFileSync).toHaveBeenCalledTimes(2);
|
||||
expect(readFileSync).toHaveBeenCalledTimes(3); // key, cert, data/robot/robot.json
|
||||
expect(httpsModuleMock.createServer).toHaveBeenCalledWith({
|
||||
enable: true,
|
||||
key: 'fake-key',
|
||||
@@ -61,7 +61,8 @@ describe('startRobot orchestrator', () => {
|
||||
expect.objectContaining({ name: 'Base', instance: expect.any(Object) }),
|
||||
expect.objectContaining({ name: 'Elbow', instance: expect.any(Object) }),
|
||||
expect.objectContaining({ name: 'Hand', instance: expect.any(Object) })
|
||||
])
|
||||
]),
|
||||
expect.objectContaining({}) // options: { apiKey }
|
||||
);
|
||||
|
||||
expect(httpsServerMock.listen).toHaveBeenCalledWith(2095);
|
||||
|
||||
Reference in New Issue
Block a user