model-explorer / js /tests /layerStats.test.js
mr4's picture
Upload 71 files
9bd422a verified
/**
* Unit tests for LayerStats.compute()
* Validates: Requirements 19.1, 19.2, 19.4, 19.5
*/
import { describe, it, expect } from 'vitest';
// Extract the pure compute logic for testing (mirrors LayerStats.compute)
function computeLayerStats(parsedModel) {
const nodes = (parsedModel && parsedModel.graph && parsedModel.graph.nodes) ? parsedModel.graph.nodes : [];
const edges = (parsedModel && parsedModel.graph && parsedModel.graph.edges) ? parsedModel.graph.edges : [];
const initializers = (parsedModel && parsedModel.initializers) ? parsedModel.initializers : [];
const totalNodes = nodes.length;
const totalEdges = edges.length;
const totalInitializers = initializers.length;
const countMap = {};
nodes.forEach((node) => {
const op = node.opType || 'Unknown';
countMap[op] = (countMap[op] || 0) + 1;
});
const opTypeCounts = Object.entries(countMap)
.map(([opType, count]) => ({
opType,
count,
percentage: totalNodes > 0 ? parseFloat(((count / totalNodes) * 100).toFixed(1)) : 0,
}))
.sort((a, b) => b.count - a.count);
return { opTypeCounts, totalNodes, totalEdges, totalInitializers };
}
describe('LayerStats.compute', () => {
it('should count operator types correctly', () => {
const model = {
graph: {
nodes: [
{ id: '1', opType: 'Conv' },
{ id: '2', opType: 'ReLU' },
{ id: '3', opType: 'Conv' },
{ id: '4', opType: 'Conv' },
{ id: '5', opType: 'ReLU' },
],
edges: [{ source: '1', target: '2' }],
},
initializers: [{ name: 'w1' }, { name: 'w2' }],
};
const stats = computeLayerStats(model);
expect(stats.totalNodes).toBe(5);
expect(stats.totalEdges).toBe(1);
expect(stats.totalInitializers).toBe(2);
expect(stats.opTypeCounts).toHaveLength(2);
// Sorted descending by count
expect(stats.opTypeCounts[0]).toEqual({ opType: 'Conv', count: 3, percentage: 60 });
expect(stats.opTypeCounts[1]).toEqual({ opType: 'ReLU', count: 2, percentage: 40 });
});
it('should return sum of counts equal to totalNodes', () => {
const model = {
graph: {
nodes: [
{ id: '1', opType: 'Conv' },
{ id: '2', opType: 'BatchNorm' },
{ id: '3', opType: 'Conv' },
{ id: '4', opType: 'Add' },
],
edges: [],
},
initializers: [],
};
const stats = computeLayerStats(model);
const sumCounts = stats.opTypeCounts.reduce((s, e) => s + e.count, 0);
expect(sumCounts).toBe(stats.totalNodes);
});
it('should handle empty model gracefully', () => {
const stats = computeLayerStats({ graph: { nodes: [], edges: [] }, initializers: [] });
expect(stats.totalNodes).toBe(0);
expect(stats.totalEdges).toBe(0);
expect(stats.totalInitializers).toBe(0);
expect(stats.opTypeCounts).toHaveLength(0);
});
it('should handle null/undefined model', () => {
const stats = computeLayerStats(null);
expect(stats.totalNodes).toBe(0);
expect(stats.opTypeCounts).toHaveLength(0);
});
it('should label nodes without opType as Unknown', () => {
const model = {
graph: { nodes: [{ id: '1' }, { id: '2', opType: '' }], edges: [] },
initializers: [],
};
const stats = computeLayerStats(model);
expect(stats.opTypeCounts).toHaveLength(1);
expect(stats.opTypeCounts[0].opType).toBe('Unknown');
expect(stats.opTypeCounts[0].count).toBe(2);
});
it('should sort by count descending', () => {
const model = {
graph: {
nodes: [
{ id: '1', opType: 'A' },
{ id: '2', opType: 'B' },
{ id: '3', opType: 'B' },
{ id: '4', opType: 'C' },
{ id: '5', opType: 'C' },
{ id: '6', opType: 'C' },
],
edges: [],
},
initializers: [],
};
const stats = computeLayerStats(model);
expect(stats.opTypeCounts[0].opType).toBe('C');
expect(stats.opTypeCounts[1].opType).toBe('B');
expect(stats.opTypeCounts[2].opType).toBe('A');
});
it('should compute percentages that sum close to 100', () => {
const model = {
graph: {
nodes: [
{ id: '1', opType: 'Conv' },
{ id: '2', opType: 'Conv' },
{ id: '3', opType: 'ReLU' },
],
edges: [],
},
initializers: [],
};
const stats = computeLayerStats(model);
const sumPct = stats.opTypeCounts.reduce((s, e) => s + e.percentage, 0);
// Allow small rounding tolerance
expect(Math.abs(sumPct - 100)).toBeLessThan(1);
});
});