import { describe, it, expect, beforeEach } from 'vitest';
import { LadderEngine } from './engine';
import type { Program, Rung } from './types';
import { MathOperationError, InvalidMemoryAccessError } from './errors';
describe('Hardening: Math and Error Handling', () => {
let engine: LadderEngine;
beforeEach(() => {
engine = new LadderEngine();
});
describe('Math Operations', () => {
it('performs ADD operation', () => {
const rung: Rung = {
id: 'r1',
enabled: true,
series: [
{ type: 'math', operator: 'ADD', inputA: 'I:1', inputB: 'I:2', outputAddress: 'M:1' },
{ type: 'coil', coilType: 'OUTPUT', address: 'Q:1' }
]
};
engine.loadProgram({ name: 'math-test', rungs: [rung], cycleTime: 100 });
engine.setInput('I:1', 10.5);
engine.setInput('I:2', 5.5);
engine.scan();
expect(engine.getMemory('M:1')).toBe(16);
});
it('performs SUB operation', () => {
const rung: Rung = {
id: 'r1',
enabled: true,
series: [
{ type: 'math', operator: 'SUB', inputA: 'I:1', inputB: 'I:2', outputAddress: 'M:1' },
{ type: 'coil', coilType: 'OUTPUT', address: 'Q:1' }
]
};
engine.loadProgram({ name: 'math-test', rungs: [rung], cycleTime: 100 });
engine.setInput('I:1', 10);
engine.setInput('I:2', 4.5);
engine.scan();
expect(engine.getMemory('M:1')).toBe(5.5);
});
it('performs MUL operation', () => {
const rung: Rung = {
id: 'r1',
enabled: true,
series: [
{ type: 'math', operator: 'MUL', inputA: 'I:1', inputB: 'I:2', outputAddress: 'M:1' },
{ type: 'coil', coilType: 'OUTPUT', address: 'Q:1' }
]
};
engine.loadProgram({ name: 'math-test', rungs: [rung], cycleTime: 100 });
engine.setInput('I:1', 3);
engine.setInput('I:2', 2.5);
engine.scan();
expect(engine.getMemory('M:1')).toBe(7.5);
});
it('performs DIV operation', () => {
const rung: Rung = {
id: 'r1',
enabled: true,
series: [
{ type: 'math', operator: 'DIV', inputA: 'I:1', inputB: 'I:2', outputAddress: 'M:1' },
{ type: 'coil', coilType: 'OUTPUT', address: 'Q:1' }
]
};
engine.loadProgram({ name: 'math-test', rungs: [rung], cycleTime: 100 });
engine.setInput('I:1', 10);
engine.setInput('I:2', 4);
engine.scan();
expect(engine.getMemory('M:1')).toBe(2.5);
});
it('throws error on division by zero', () => {
const rung: Rung = {
id: 'r1',
enabled: true,
series: [
{ type: 'math', operator: 'DIV', inputA: 'I:1', inputB: 'I:2', outputAddress: 'M:1' },
{ type: 'coil', coilType: 'OUTPUT', address: 'Q:1' }
]
};
engine.loadProgram({ name: 'math-test', rungs: [rung], cycleTime: 100 });
engine.setInput('I:1', 10);
engine.setInput('I:2', 0);
// Since errors are caught in evaluateSeries and logged to console,
// we might not see them thrown here directly if we just call scan().
// But if we want to test the logic, we should check how the engine handles it.
// The engine catches it and sets result to false.
engine.scan();
expect(engine.getOutput('Q:1')).toBe(false);
});
});
describe('Error Handling', () => {
it('handles invalid memory access gracefully', () => {
const rung: Rung = {
id: 'r1',
enabled: true,
series: [
{ type: 'contact', contactType: 'NO', address: 'INVALID_ADDR' },
{ type: 'coil', coilType: 'OUTPUT', address: 'Q:1' }
]
};
engine.loadProgram({ name: 'error-test', rungs: [rung], cycleTime: 100 });
engine.scan();
// Should not crash, and because contact access failed, Q:1 should be false
expect(engine.getOutput('Q:1')).toBe(false);
});
});
describe('State Snapshots', () => {
it('can take and restore a snapshot', () => {
engine.setInput('I:0.0', true);
engine.setMemory('M:0.0', true);
const snapshot = engine.takeSnapshot();
// Modify state
engine.setInput('I:0.0', false);
engine.setMemory('M:0.0', false);
expect(engine.getInput('I:0.0')).toBe(false);
expect(engine.getMemory('M:0.0')).toBe(false);
// Restore
engine.restoreSnapshot(snapshot);
expect(engine.getInput('I:0.0')).toBe(true);
expect(engine.getMemory('M:0.0')).toBe(true);
});
it('ensures snapshot is a deep copy', () => {
engine.setInput('I:0.0', true);
const snapshot = engine.takeSnapshot();
engine.setInput('I:0.0', false);
expect(snapshot.inputs.get('I:0.0')?.value).toBe(true);
expect(engine.getInput('I:0.0')).toBe(false);
});
});
});