test(examples): audit every shipped component value against physical ranges
The night-light LDR shipped a 0.001 ohm dark resistance for months and nothing complained: the netlist built, ngspice converged, the part rendered, and the example just quietly read 4095 forever. A wrong value is invisible, so check them all. Parses every resistor / pot / photoresistor / capacitor / inductor value in the gallery with the real parser and asserts it lands in a physically sane range. The failure message names both SPICE suffix traps, because they are the ones that bite: M is milli (mega is "Meg"), and F is femto and is deliberately not stripped, so "10uF" would parse as 10 fF rather than the 10 uF intended. Verified the check actually bites: putting "1M" back makes it fail with `dark = "1M" -> 0.001 — LDR dark resistance (ohm) out of range`. A second case asserts the audit visits a meaningful number of components, so the test can't silently degrade into a no-op. Swept the rest of the catalogue while here (all example files, the pro overlay, and the 49 100-days diagrams): no other value hits either trap.
This commit is contained in:
parent
9b0b4bc038
commit
979a95fef9
|
|
@ -0,0 +1,116 @@
|
|||
/**
|
||||
* Audit every component value shipped in the gallery examples.
|
||||
*
|
||||
* Component values go through `parseValueWithUnits`, which follows the SPICE
|
||||
* suffix convention. Two of its rules routinely trip up whoever authors an
|
||||
* example, because both produce a *silently* wrong circuit rather than an
|
||||
* error:
|
||||
*
|
||||
* - `M` is MILLI, not mega. `"1M"` is 0.001, and mega is `"1Meg"`.
|
||||
* 100d-auto-night-light shipped `dark: "1M"` for a photoresistor: the
|
||||
* 0.001 ohm "dark resistance" shorted the ADC node to VCC, so the sketch
|
||||
* read 4095 forever and its lux control did nothing.
|
||||
* - `F` is FEMTO, and is deliberately NOT stripped as a unit letter, so
|
||||
* `"10uF"` parses as `10 * 1e-15` — nine orders of magnitude off the
|
||||
* 10 microfarads the author meant. Write `"10u"`.
|
||||
*
|
||||
* A wrong value here is invisible: the netlist builds, ngspice converges, the
|
||||
* part renders, and the example just quietly misbehaves. So assert the parsed
|
||||
* numbers land inside physically sane ranges for their component class.
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { exampleProjects } from '../data/examples';
|
||||
import { parseValueWithUnits } from '../simulation/spice/valueParser';
|
||||
|
||||
/** Physically plausible ranges, per (metadataId prefix, property). */
|
||||
const RANGES: Array<{
|
||||
match: (metaId: string) => boolean;
|
||||
prop: string;
|
||||
min: number;
|
||||
max: number;
|
||||
what: string;
|
||||
}> = [
|
||||
// A resistor below an ohm is a wire; above 100 M it's an open.
|
||||
{ match: (m) => m === 'resistor' || m.startsWith('resistor-'), prop: 'value', min: 1, max: 1e8, what: 'resistance (ohm)' },
|
||||
{ match: (m) => m === 'potentiometer', prop: 'value', min: 10, max: 1e7, what: 'pot resistance (ohm)' },
|
||||
// LDR dark resistance: real parts sit between ~10k and ~20M.
|
||||
{ match: (m) => m === 'photoresistor' || m === 'photoresistor-sensor', prop: 'dark', min: 1e3, max: 1e8, what: 'LDR dark resistance (ohm)' },
|
||||
{ match: (m) => m === 'photoresistor' || m === 'photoresistor-sensor', prop: 'pullup', min: 100, max: 1e7, what: 'LDR pull-down (ohm)' },
|
||||
// Capacitors: 1 pF .. 1 F covers everything from RF trimmers to supercaps.
|
||||
{ match: (m) => m === 'capacitor' || m === 'capacitor-electrolytic' || m.startsWith('capacitor-'), prop: 'value', min: 1e-12, max: 1, what: 'capacitance (F)' },
|
||||
// Inductors: 1 nH .. 10 H.
|
||||
{ match: (m) => m === 'inductor' || m.startsWith('inductor-'), prop: 'value', min: 1e-9, max: 10, what: 'inductance (H)' },
|
||||
];
|
||||
|
||||
interface Offender {
|
||||
example: string;
|
||||
componentId: string;
|
||||
metadataId: string;
|
||||
prop: string;
|
||||
raw: unknown;
|
||||
parsed: number;
|
||||
what: string;
|
||||
}
|
||||
|
||||
function audit(): Offender[] {
|
||||
const out: Offender[] = [];
|
||||
for (const ex of exampleProjects) {
|
||||
for (const comp of ex.components ?? []) {
|
||||
// Example components carry the branded tag (`wokwi-resistor`); the
|
||||
// mapper sees the stripped metadata id. Compare on both.
|
||||
const raw = String((comp as { type?: string }).type ?? '');
|
||||
const metaId = raw.replace(/^(wokwi|velxio|chip)-/, '');
|
||||
const props = (comp as { properties?: Record<string, unknown> }).properties ?? {};
|
||||
for (const rule of RANGES) {
|
||||
if (!rule.match(metaId)) continue;
|
||||
if (!(rule.prop in props)) continue;
|
||||
const rawVal = props[rule.prop];
|
||||
const parsed = parseValueWithUnits(rawVal, NaN);
|
||||
if (!Number.isFinite(parsed) || parsed < rule.min || parsed > rule.max) {
|
||||
out.push({
|
||||
example: ex.id,
|
||||
componentId: String((comp as { id?: string }).id ?? '?'),
|
||||
metadataId: metaId,
|
||||
prop: rule.prop,
|
||||
raw: rawVal,
|
||||
parsed,
|
||||
what: rule.what,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
describe('gallery example component values', () => {
|
||||
it('parse into physically sane ranges', () => {
|
||||
const offenders = audit();
|
||||
const report = offenders
|
||||
.map(
|
||||
(o) =>
|
||||
`${o.example} / ${o.componentId} (${o.metadataId}).${o.prop} = ` +
|
||||
`${JSON.stringify(o.raw)} -> ${o.parsed} — ${o.what} out of range. ` +
|
||||
`Remember: SPICE M = milli (use "Meg"), F = femto (drop the F).`,
|
||||
)
|
||||
.join('\n');
|
||||
expect(report).toBe('');
|
||||
});
|
||||
|
||||
it('covers a meaningful number of components (guard against a silent no-op)', () => {
|
||||
let checked = 0;
|
||||
for (const ex of exampleProjects) {
|
||||
for (const comp of ex.components ?? []) {
|
||||
const metaId = String((comp as { type?: string }).type ?? '').replace(
|
||||
/^(wokwi|velxio|chip)-/,
|
||||
'',
|
||||
);
|
||||
const props = (comp as { properties?: Record<string, unknown> }).properties ?? {};
|
||||
for (const rule of RANGES) {
|
||||
if (rule.match(metaId) && rule.prop in props) checked++;
|
||||
}
|
||||
}
|
||||
}
|
||||
expect(checked).toBeGreaterThan(50);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue