Numbered cards for the text sections (1.1-1.3, 2, 4, 11, 12) on the overview, domain interfaces on the technical context, black-box descriptions for every building block, blocks mapped to nodes in the deployment view, crosscutting concepts on level 1, and a 1000-quality folder with the quality tree. The example test checks each section's card.
123 lines
6.7 KiB
TypeScript
123 lines
6.7 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { test } from 'node:test';
|
|
import { parse } from 'yaml';
|
|
import { validate } from '../src/archify/cli';
|
|
import { EXAMPLE_PREFIX, MODEL_EXAMPLES, mainFromExample } from '../src/language/examples';
|
|
import { MAIN_FILE, REGISTRY_EXT, hasDiagramBody, mergeRegistries, parseRegistry, readModelMeta, resolveRefs } from '../src/model/format';
|
|
import { DECISION_EXT, mergeDecisions, parseDecision, validateDecisions } from '../src/model/decisions';
|
|
import { readDiagramSource } from '../src/sources/detect';
|
|
|
|
const root = path.resolve(__dirname, '..', '..');
|
|
const examples = path.join(root, 'templates', 'models');
|
|
const runtime = {
|
|
cliPath: path.join(root, 'vendor', 'archify', 'bin', 'archify.mjs'),
|
|
nodePath: process.execPath,
|
|
runAsNode: false,
|
|
workDir: fs.mkdtempSync(path.join(os.tmpdir(), 'archify-ext-examples-')),
|
|
};
|
|
|
|
function walk(dir: string, rel = ''): string[] {
|
|
return fs.readdirSync(path.join(dir, rel), { withFileTypes: true }).flatMap((entry) => {
|
|
const next = rel ? `${rel}/${entry.name}` : entry.name;
|
|
return entry.isDirectory() ? walk(dir, next) : [next];
|
|
});
|
|
}
|
|
|
|
// The folders each framework's examples must have, besides main.archify, registry/ and decisions/.
|
|
const FRAMEWORK_FOLDERS: Record<string, string[]> = {
|
|
c4: ['0100-context', '0200-containers', '0300-components'],
|
|
arc42: ['0300-context', '0500-building-blocks', '0600-runtime', '0700-deployment', '1000-quality'],
|
|
archimate: ['0100-business', '0200-application', '0300-technology'],
|
|
};
|
|
|
|
test('every example folder is listed, every listed example exists, three examples per framework', () => {
|
|
const folders = fs.readdirSync(examples, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort();
|
|
assert.deepEqual(MODEL_EXAMPLES.map((e) => e.folder).sort(), folders);
|
|
for (const [framework, label] of [['c4', 'C4'], ['arc42', 'arc42'], ['archimate', 'ArchiMate']]) {
|
|
const listed = MODEL_EXAMPLES.filter((e) => e.folder.startsWith(`${framework}-`));
|
|
assert.equal(listed.length, 3, framework);
|
|
for (const e of listed) assert.ok(e.label.startsWith(`${label} - `), e.label);
|
|
}
|
|
});
|
|
|
|
for (const example of MODEL_EXAMPLES) {
|
|
test(`example ${example.folder} is a complete model that validates`, async () => {
|
|
const dir = path.join(examples, example.folder);
|
|
const files = walk(dir);
|
|
const read = (rel: string) => fs.readFileSync(path.join(dir, rel), 'utf8');
|
|
|
|
// main.archify: model block with id/name/description and an overview diagram.
|
|
assert.ok(files.includes(MAIN_FILE));
|
|
const main = parse(read(MAIN_FILE));
|
|
const meta = readModelMeta(main);
|
|
assert.equal(meta?.id, example.folder.replace(EXAMPLE_PREFIX, ''));
|
|
assert.ok(meta?.name && meta.description);
|
|
assert.ok(hasDiagramBody(main));
|
|
|
|
// Registry: at least people, systems and containers; no problems.
|
|
const registryFiles = files.filter((f) => f.endsWith(REGISTRY_EXT)).sort();
|
|
assert.ok(registryFiles.length >= 3, `registry files: ${registryFiles.join(', ')}`);
|
|
const registry = mergeRegistries(registryFiles.map((file) => ({ file, ...parseRegistry(read(file), file) })));
|
|
assert.deepEqual(registry.problems, []);
|
|
for (const element of registry.elements.values()) {
|
|
assert.ok(element.type && element.label && element.description, `${element.id} needs type, label and description`);
|
|
if (element.diagram) assert.ok(files.includes(element.diagram), `${element.id} links to missing ${element.diagram}`);
|
|
}
|
|
|
|
// Decisions: at least one, all links resolve.
|
|
const decisionFiles = files.filter((f) => f.endsWith(DECISION_EXT));
|
|
assert.ok(decisionFiles.length >= 1, 'an example carries at least one decision');
|
|
const decisions = mergeDecisions(decisionFiles.map((file) => ({ file, ...parseDecision(read(file), file) })));
|
|
assert.deepEqual(decisions.problems, []);
|
|
const diagramPaths = new Set(files.filter((f) => f.endsWith('.archify') && f !== MAIN_FILE));
|
|
assert.deepEqual(validateDecisions(decisions, registry, diagramPaths), []);
|
|
for (const d of decisions.decisions.values()) assert.ok(d.title && d.status && d.context && d.decision && d.consequences, `${d.id} is complete`);
|
|
|
|
// Diagrams: the framework's folders, each diagram resolving without problems and valid for Archify.
|
|
const diagrams = files.filter((f) => f.endsWith('.archify')).sort();
|
|
for (const level of FRAMEWORK_FOLDERS[example.folder.split('-')[0]]) {
|
|
assert.ok(diagrams.some((d) => d.startsWith(`${level}/`)), `missing ${level}`);
|
|
}
|
|
const used = new Set<string>();
|
|
for (const file of diagrams) {
|
|
const source = readDiagramSource(read(file), file);
|
|
assert.ok(source, `${file} is not a diagram`);
|
|
const resolved = resolveRefs(source.diagram, { registry, decisions: new Set(decisions.decisions.keys()), fileName: file });
|
|
assert.deepEqual(resolved.problems, [], file);
|
|
for (const link of resolved.links.values()) assert.ok(files.includes(link), `${file} links to missing ${link}`);
|
|
for (const id of resolved.used) used.add(id);
|
|
const result = await validate(runtime, source.type, JSON.stringify(resolved.diagram));
|
|
assert.deepEqual(result.diagnostics.filter((d) => d.severity === 'error').map((d) => `${file}: ${d.code}: ${d.message}`), []);
|
|
assert.ok(result.ok, file);
|
|
}
|
|
// Every registry element is used by some diagram.
|
|
for (const id of registry.elements.keys()) assert.ok(used.has(id), `element ${id} is never used`);
|
|
|
|
// arc42: every text section has a card somewhere, titled with its number.
|
|
if (example.folder.startsWith('arc42-')) {
|
|
const titles = diagrams.flatMap((file) => {
|
|
const cards = parse(read(file))?.cards;
|
|
return Array.isArray(cards) ? cards.map((card: { title?: string }) => String(card?.title ?? '')) : [];
|
|
});
|
|
for (const section of ['1.1', '1.2', '1.3', '2', '3', '4', '7', '8', '10', '11', '12']) {
|
|
assert.ok(titles.some((title) => title.startsWith(`${section} `)), `${example.folder}: no card for arc42 section ${section}`);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
test('mainFromExample keeps the example as written, or renames it after the folder', () => {
|
|
const example = MODEL_EXAMPLES[1];
|
|
const text = fs.readFileSync(path.join(examples, example.folder, MAIN_FILE), 'utf8');
|
|
assert.equal(mainFromExample(text, example.folder, example), text);
|
|
const renamed = parse(mainFromExample(text, 'shop two', example));
|
|
assert.equal(renamed.model.id, 'shop-two');
|
|
assert.equal(renamed.model.name, 'Shop two');
|
|
assert.equal(renamed.meta.title, 'Shop two — overview');
|
|
assert.equal(renamed.model.description, parse(text).model.description);
|
|
assert.deepEqual(renamed.components, parse(text).components);
|
|
});
|