mirror of
https://github.com/m3y54m/Embedded-Engineering-Roadmap.git
synced 2026-10-11 00:38:24 +03:00
103 lines
5.9 KiB
JavaScript
103 lines
5.9 KiB
JavaScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { readFileSync } from 'node:fs';
|
|
import { parseRoadmap, renderInline } from './parser.js';
|
|
import { linkDiagram } from './diagram.js';
|
|
import { SPACE, buildLayout } from './layout.js';
|
|
|
|
const data = parseRoadmap(readFileSync(new URL('../README.md', import.meta.url), 'utf8'));
|
|
const find = (title) => data.topics.find((t) => t.title === title);
|
|
const linked = (a, b) => data.links.find((l) => (l.source === a && l.target === b) || (l.source === b && l.target === a));
|
|
|
|
test('builds the topic tree from the README headings', () => {
|
|
assert.ok(data.topics.length > 100);
|
|
assert.equal(find('Microcontrollers').depth, 1);
|
|
assert.ok(find('Microcontrollers').children.some((c) => c.title === 'GPIO'));
|
|
assert.equal(find('1-Wire').parent.title, 'Basic Protocols');
|
|
assert.equal(find('High-Performance Computing (HPC)').parent.title, 'Multithreading & Parallel Processing');
|
|
assert.equal(find('History'), undefined);
|
|
assert.deepEqual(data.groups.map((g) => g.title), ["Don't Know Where to Start!", 'Learning Resources']);
|
|
});
|
|
|
|
test('gives every topic a unique slug', () => {
|
|
assert.equal(new Set(data.topics.map((t) => t.slug)).size, data.topics.length);
|
|
});
|
|
|
|
test('parses resource markers, bracketed titles and URLs with parentheses', () => {
|
|
const beej = find('C').resources.find((r) => r.title.startsWith("Beej's Guide"));
|
|
assert.deepEqual([beej.type, beej.beginner, beej.gem], ['book', true, false]);
|
|
const j1939 = find('CAN').resources.find((r) => r.title.startsWith('J1939'));
|
|
assert.equal(j1939.title, 'J1939 Explained - A Simple Intro [v2.0 | 2021]');
|
|
assert.match(find('PCM').resources[0].url, /_04\(1\)\.pdf$/);
|
|
assert.equal(find('GoogleTest - Google Testing and Mocking Framework').resources[0].url, 'https://github.com/google/googletest');
|
|
assert.match(find('Career Development').resources[0].note, /job board/);
|
|
});
|
|
|
|
test('keeps descriptions and the README introduction', () => {
|
|
assert.match(data.root.description[0].text, /^This roadmap is designed/);
|
|
assert.match(find('Microcontrollers').description[0].text, /^Microcontrollers are integrated circuits/);
|
|
});
|
|
|
|
test('links topics that share a resource or mention each other', () => {
|
|
assert.ok(linked(find('Docker'), find('CI/CD Pipelines')).shared.length > 0);
|
|
assert.ok(linked(find('I2C'), find('Sensors')).mentions.length > 0);
|
|
assert.equal(linked(find('Microcontrollers'), find('GPIO')), undefined, 'parent and child are not cross-linked');
|
|
assert.equal(linked(find('Thread'), find('RT-Thread')), undefined, 'RT-Thread is not a mention of Thread');
|
|
});
|
|
|
|
test('renderInline escapes HTML and only links http(s) URLs', () => {
|
|
assert.equal(
|
|
renderInline('<b>x</b> **y** [a](https://e.com/?q=1&r=2) [j](javascript:alert(1))'),
|
|
'<b>x</b> <strong>y</strong> <a href="https://e.com/?q=1&r=2" target="_blank" rel="noopener noreferrer">a</a> [j](javascript:alert(1))',
|
|
);
|
|
});
|
|
|
|
const map = JSON.parse(readFileSync(new URL('./map.json', import.meta.url), 'utf8'));
|
|
|
|
test('places topics in the map areas and their cross-section', () => {
|
|
const plan = linkDiagram(map, data);
|
|
assert.deepEqual(plan.unmatched, [], 'every map topic matches a README heading (or a "readme" entry in map.json)');
|
|
assert.deepEqual(plan.invalidReadme, [], 'map.json "readme" entries name a topic on the map and existing README headings');
|
|
assert.deepEqual(plan.invalid, [], 'importance is required, recommended or possible');
|
|
assert.deepEqual(find('GPIO').areas, ['SOFTWARE', 'HARDWARE']);
|
|
assert.deepEqual(find('I2C').areas, ['SOFTWARE', 'HARDWARE']);
|
|
assert.deepEqual(find('Sensors & Actuators').areas, ['SOFTWARE', 'HARDWARE']);
|
|
assert.deepEqual(find('GDB').areas, ['SOFTWARE']);
|
|
assert.deepEqual(find('Oscilloscope').areas, ['HARDWARE']);
|
|
assert.deepEqual(find('Soft Skills').areas, ['SOFT SKILLS']);
|
|
assert.deepEqual(find('Microcontrollers').areas, ['SOFTWARE', 'HARDWARE']);
|
|
assert.equal(find('RTOS Basics').importance, 'required');
|
|
assert.equal(find('Rust').importance, 'possible');
|
|
assert.deepEqual(find('Linux Device Drivers').areas, ['SOFTWARE']);
|
|
assert.equal(find('Projects').areas.length, 0, 'getting-started topics are not on the map');
|
|
});
|
|
|
|
test('every map topic is connected to related topics, each connection with a reason', () => {
|
|
const plan = linkDiagram(map, data);
|
|
assert.deepEqual(plan.invalidLinks, [], 'map.json "links" name existing topics, once, with a reason');
|
|
// Soft skills describe the engineer, not the technology, so some of them stand alone.
|
|
const alone = plan.boxes.filter((b) => !b.header && !b.areas.includes('SOFT SKILLS') && !b.related.length);
|
|
assert.deepEqual(alone.map((b) => b.text), [], 'topics without a connection');
|
|
const has = (a, b) => plan.connections.some((c) => c.reason && [c.a.text, c.b.text].sort().join('|') === [a, b].sort().join('|'));
|
|
assert.ok(has('C', 'Memory Management'));
|
|
assert.ok(has('Rust', 'Memory Management'));
|
|
});
|
|
|
|
test('lays the map out on one A4 page with a minimum spacing between boxes', () => {
|
|
const layout = buildLayout(linkDiagram(map, data));
|
|
assert.deepEqual(layout.missing, [], 'every cluster in map.json has a place in layout.js');
|
|
assert.ok(layout.height / layout.width <= Math.SQRT2, 'fits A4 portrait');
|
|
const boxes = layout.items.filter((i) => i.kind === 'box');
|
|
const inside = (a, b) => a.x >= b.x && a.y >= b.y && a.x + a.w <= b.x + b.w && a.y + a.h <= b.y + b.h;
|
|
const gap = (a, b) => Math.max(b.x - a.x - a.w, a.x - b.x - b.w, b.y - a.y - a.h, a.y - b.y - b.h);
|
|
for (const [i, a] of boxes.entries()) {
|
|
for (const b of boxes.slice(i + 1)) assert.ok(gap(a, b) >= SPACE - 0.01, `${a.text} / ${b.text}`);
|
|
for (const g of layout.groups) {
|
|
if (!inside(a, g.rect)) continue;
|
|
const r = g.rect;
|
|
const d = Math.min(a.x - r.x, a.y - r.y, r.x + r.w - a.x - a.w, r.y + r.h - a.y - a.h);
|
|
assert.ok(d >= SPACE - 0.01, `${a.text} too close to the ${g.name} outline`);
|
|
}
|
|
}
|
|
});
|