138 lines
5.3 KiB
TypeScript
138 lines
5.3 KiB
TypeScript
|
|
/**
|
||
|
|
* Contract tests for the SolverPort interface.
|
||
|
|
*
|
||
|
|
* Any adapter (real or fake) must satisfy these invariants. Today
|
||
|
|
* `FakeSolverAdapter` is the SUT; once `NgSpiceNodeAdapter` lands
|
||
|
|
* (sub-step F1) it will run the same suite to confirm it honours the
|
||
|
|
* port contract exactly the same way.
|
||
|
|
*
|
||
|
|
* Contract:
|
||
|
|
* - init() is idempotent
|
||
|
|
* - loadCircuit() can be called multiple times (replaces previous)
|
||
|
|
* - solve() returns a SolveResult with vectors keyed by lower-case name
|
||
|
|
* - solve() ignores requested vectors that aren't in the engine state
|
||
|
|
* - alterSource() doesn't return data
|
||
|
|
* - dispose() is the final call; subsequent uses are caller's bug
|
||
|
|
*/
|
||
|
|
import { describe, it, expect } from 'vitest';
|
||
|
|
import { FakeSolverAdapter } from '../simulation/spice/adapters/FakeSolverAdapter';
|
||
|
|
|
||
|
|
describe('SolverPort contract — FakeSolverAdapter', () => {
|
||
|
|
it('init() is idempotent and counted per call', async () => {
|
||
|
|
const fake = new FakeSolverAdapter();
|
||
|
|
await fake.init();
|
||
|
|
await fake.init();
|
||
|
|
await fake.init();
|
||
|
|
expect(fake.calls.init).toBe(3);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('loadCircuit() records every submission', async () => {
|
||
|
|
const fake = new FakeSolverAdapter();
|
||
|
|
await fake.loadCircuit('first netlist');
|
||
|
|
await fake.loadCircuit('second netlist');
|
||
|
|
expect(fake.calls.loadCircuit).toEqual(['first netlist', 'second netlist']);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('solve() returns SolveResult with requested vectors', async () => {
|
||
|
|
const fake = new FakeSolverAdapter({
|
||
|
|
vectors: { 'v(n0)': 4.97, 'v(n1)': 0.1, 'i(v_sense)': 0.005 },
|
||
|
|
});
|
||
|
|
const result = await fake.solve(
|
||
|
|
{ kind: 'op' },
|
||
|
|
{ vectorsOfInterest: ['v(n0)', 'v(n1)', 'i(v_sense)'] },
|
||
|
|
);
|
||
|
|
expect(result.analysis).toEqual({ kind: 'op' });
|
||
|
|
expect(result.vectors.get('v(n0)')?.real[0]).toBeCloseTo(4.97);
|
||
|
|
expect(result.vectors.get('v(n1)')?.real[0]).toBeCloseTo(0.1);
|
||
|
|
expect(result.vectors.get('i(v_sense)')?.real[0]).toBeCloseTo(0.005);
|
||
|
|
expect(result.warnings).toEqual([]);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('solve() omits requested vectors that are missing from engine state', async () => {
|
||
|
|
const fake = new FakeSolverAdapter({ vectors: { 'v(present)': 1.5 } });
|
||
|
|
const result = await fake.solve(
|
||
|
|
{ kind: 'op' },
|
||
|
|
{ vectorsOfInterest: ['v(present)', 'v(missing)'] },
|
||
|
|
);
|
||
|
|
expect(result.vectors.has('v(present)')).toBe(true);
|
||
|
|
expect(result.vectors.has('v(missing)')).toBe(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('solve() normalises vector names to lower-case', async () => {
|
||
|
|
const fake = new FakeSolverAdapter({ vectors: { 'v(out)': 3.3 } });
|
||
|
|
const result = await fake.solve({ kind: 'op' }, { vectorsOfInterest: ['V(OUT)'] });
|
||
|
|
expect(result.vectors.has('v(out)')).toBe(true);
|
||
|
|
expect(result.vectors.has('V(OUT)')).toBe(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('solve() honours dynamic vector supplier', async () => {
|
||
|
|
let voltage = 1.0;
|
||
|
|
const fake = new FakeSolverAdapter({
|
||
|
|
vectors: () => ({ 'v(out)': voltage }),
|
||
|
|
});
|
||
|
|
const r1 = await fake.solve({ kind: 'op' }, { vectorsOfInterest: ['v(out)'] });
|
||
|
|
expect(r1.vectors.get('v(out)')?.real[0]).toBeCloseTo(1.0);
|
||
|
|
voltage = 4.5;
|
||
|
|
const r2 = await fake.solve({ kind: 'op' }, { vectorsOfInterest: ['v(out)'] });
|
||
|
|
expect(r2.vectors.get('v(out)')?.real[0]).toBeCloseTo(4.5);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('alterSource() records calls and invokes onAlter hook', async () => {
|
||
|
|
const fake = new FakeSolverAdapter();
|
||
|
|
let lastAlter: [string, number] | null = null;
|
||
|
|
fake.onAlter = (name, value) => {
|
||
|
|
lastAlter = [name, value];
|
||
|
|
};
|
||
|
|
await fake.alterSource('V_uno_9', 5);
|
||
|
|
expect(fake.calls.alterSource).toEqual([['V_uno_9', 5]]);
|
||
|
|
expect(lastAlter).toEqual(['V_uno_9', 5]);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('exposes a .tran-shaped result when requested', async () => {
|
||
|
|
const fake = new FakeSolverAdapter({
|
||
|
|
vectors: { 'v(out)': new Float64Array([0, 1, 2, 3, 4]) },
|
||
|
|
timeAxis: new Float64Array([0, 1e-4, 2e-4, 3e-4, 4e-4]),
|
||
|
|
});
|
||
|
|
const result = await fake.solve(
|
||
|
|
{ kind: 'tran', step: '1e-4', stop: '4e-4' },
|
||
|
|
{ vectorsOfInterest: ['v(out)'] },
|
||
|
|
);
|
||
|
|
expect(result.timeAxis.length).toBe(5);
|
||
|
|
expect(result.vectors.get('v(out)')?.real.length).toBe(5);
|
||
|
|
expect(result.vectors.get('v(out)')?.real[3]).toBe(3);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('solve records all calls with the analysis + vectorsOfInterest', async () => {
|
||
|
|
const fake = new FakeSolverAdapter();
|
||
|
|
await fake.solve({ kind: 'op' }, { vectorsOfInterest: ['v(a)'] });
|
||
|
|
await fake.solve(
|
||
|
|
{ kind: 'tran', step: '1u', stop: '10m' },
|
||
|
|
{ vectorsOfInterest: ['v(b)', 'i(v1)'] },
|
||
|
|
);
|
||
|
|
expect(fake.calls.solve).toEqual([
|
||
|
|
{ analysis: { kind: 'op' }, vectorsOfInterest: ['v(a)'] },
|
||
|
|
{
|
||
|
|
analysis: { kind: 'tran', step: '1u', stop: '10m' },
|
||
|
|
vectorsOfInterest: ['v(b)', 'i(v1)'],
|
||
|
|
},
|
||
|
|
]);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('respects solveDelayMs (race-condition tests can use this)', async () => {
|
||
|
|
const fake = new FakeSolverAdapter({ solveDelayMs: 50 });
|
||
|
|
const t0 = performance.now();
|
||
|
|
await fake.solve({ kind: 'op' }, { vectorsOfInterest: [] });
|
||
|
|
expect(performance.now() - t0).toBeGreaterThanOrEqual(45);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('dispose() is counted; resetCalls() clears the log', async () => {
|
||
|
|
const fake = new FakeSolverAdapter();
|
||
|
|
fake.dispose();
|
||
|
|
fake.dispose();
|
||
|
|
expect(fake.calls.dispose).toBe(2);
|
||
|
|
fake.resetCalls();
|
||
|
|
expect(fake.calls.dispose).toBe(0);
|
||
|
|
expect(fake.calls.init).toBe(0);
|
||
|
|
});
|
||
|
|
});
|