Files
WebMetal/frontend/src/machine/validateGraph.test.ts
T
2026-07-19 18:05:04 +02:00

127 lines
3.8 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import type { WmEdge, WmNode } from '../editor/nodeTypes'
import { portWidth, validateGraph } from './validateGraph'
let counter = 0
function node(
type: string,
name: string,
params: Record<string, string | number> = {},
): WmNode {
return {
id: `n${++counter}`,
type,
position: { x: 0, y: 0 },
data: { name, doc: '', params },
}
}
function edge(
source: WmNode,
sourceHandle: string,
target: WmNode,
targetHandle: string,
kind: 'data' | 'control' = 'data',
): WmEdge {
return {
id: `e${++counter}`,
source: source.id,
sourceHandle,
target: target.id,
targetHandle,
data: { kind },
}
}
/** Minimal graph that passes all error-level checks. */
function soundGraph() {
const pc = node('pc', 'PC1', { width: 16 })
const mem = node('memory', 'MAIN', { size: 256, width: 8 })
const nodes = [pc, mem]
const edges = [edge(pc, 'out', mem, 'addr')]
return { nodes, edges }
}
const errorsOf = (nodes: WmNode[], edges: WmEdge[]) =>
validateGraph(nodes, edges).filter((d) => d.severity === 'error')
describe('validateGraph', () => {
it('produces no errors for a sound minimal graph', () => {
const { nodes, edges } = soundGraph()
expect(errorsOf(nodes, edges)).toEqual([])
})
it('flags invalid and duplicate names as errors', () => {
const { nodes, edges } = soundGraph()
nodes.push(node('register', '2fast'), node('register', 'pc1'))
const messages = errorsOf(nodes, edges).map((d) => d.message)
expect(messages.join()).toContain('not a valid identifier')
expect(messages.join()).toContain('duplicate block name')
})
it('requires a PC and at least one memory', () => {
expect(
errorsOf([], [])
.map((d) => d.message)
.join(),
).toMatch(/Program Counter[\s\S]*Memory|Memory[\s\S]*Program Counter/)
})
it('rejects multiple PCs and multiple flags blocks', () => {
const { nodes, edges } = soundGraph()
nodes.push(
node('pc', 'PC2', { width: 16 }),
node('flags', 'FLAGS1', { flags: 'Z' }),
node('flags', 'FLAGS2', { flags: 'Z' }),
)
const messages = errorsOf(nodes, edges).map((d) => d.message)
expect(messages.join()).toContain('only one Program Counter')
expect(messages.join()).toContain('only one Flags block')
})
it('warns about unconnected inputs', () => {
const { nodes, edges } = soundGraph()
nodes.push(node('register', 'REG1', { width: 8 }))
const warnings = validateGraph(nodes, edges).filter(
(d) => d.severity === 'warning',
)
expect(warnings.map((d) => d.message).join()).toContain(
'"REG1": input "Data in" is not connected',
)
})
it('warns about data bus width mismatches', () => {
const { nodes, edges } = soundGraph()
const reg = node('register', 'REG1', { width: 8 })
const alu = node('alu', 'ALU1', { width: 16 })
nodes.push(reg, alu)
edges.push(edge(reg, 'out', alu, 'a'))
const warnings = validateGraph(nodes, edges).filter(
(d) => d.severity === 'warning',
)
expect(warnings.map((d) => d.message).join()).toContain(
'bus width mismatch',
)
})
it('ignores comment nodes entirely', () => {
const { nodes, edges } = soundGraph()
nodes.push(node('comment', 'not an identifier!!', { text: 'hi' }))
expect(errorsOf(nodes, edges)).toEqual([])
})
})
describe('portWidth', () => {
it('derives widths from params', () => {
const reg = node('register', 'R1', { width: 12 })
expect(portWidth(reg, 'in')).toBe(12)
expect(portWidth(reg, 'load')).toBeNull() // control port
const mem = node('memory', 'M1', { size: 256, width: 8 })
expect(portWidth(mem, 'addr')).toBe(8)
expect(portWidth(mem, 'dout')).toBe(8)
const flags = node('flags', 'F1', { flags: 'Z,N,C' })
expect(portWidth(flags, 'out')).toBe(3)
})
})