test(tcl): add TCL variant test suite including PCell generation
All non-TCL tests (extract, gds, drc, cif) now also run against the TCL variant using magic::-prefixed Tcl scripts. A new PCell test generates two parameterized rectangle cells and verifies their GDS output. The CI workflow runs npm run test:tcl after the TCL build and saves the generated output files as an artifact.
This commit is contained in:
parent
8fdd2eec20
commit
1f19ac913f
|
|
@ -97,7 +97,7 @@ jobs:
|
|||
- name: Run example tests (non-TCL variant)
|
||||
run: cd npm && npm test
|
||||
|
||||
- name: Run smoke test (TCL variant)
|
||||
- name: Run full test suite (TCL variant)
|
||||
run: cd npm && npm run test:tcl
|
||||
|
||||
# Dump generated text outputs (.ext, .spice, .cif, …) into the CI log
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
// all-tcl.js — Run the full test suite against the TCL variant.
|
||||
//
|
||||
// Covers all non-TCL tests (extract, gds, drc, cif) run through the
|
||||
// Tcl interpreter, plus a PCell generation test that is TCL-only.
|
||||
//
|
||||
// Usage: node examples/all-tcl.js
|
||||
import { run as runExtract } from './extract-tcl.js';
|
||||
import { run as runGds } from './gds-tcl.js';
|
||||
import { run as runDrc } from './drc-tcl.js';
|
||||
import { run as runCif } from './cif-tcl.js';
|
||||
import { run as runPcell } from './pcell.js';
|
||||
|
||||
const PAD = 9;
|
||||
|
||||
async function test(name, fn) {
|
||||
process.stdout.write(` ${name.padEnd(PAD)} `);
|
||||
try {
|
||||
const result = await fn();
|
||||
console.log(`PASS ${formatResult(name, result)}`);
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.log(`FAIL ${e.message ?? e}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function formatResult(name, r) {
|
||||
if (!r) return '';
|
||||
switch (name) {
|
||||
case 'extract': return [r.ext, r.spice].filter(Boolean).map(p => p.split(/[\\/]/).pop()).join(', ');
|
||||
case 'gds': return `${r.outPath.split(/[\\/]/).pop()} (${r.bytes} B)`;
|
||||
case 'cif': return `${r.outPath.split(/[\\/]/).pop()} (${r.bytes} B)`;
|
||||
case 'drc': return r.violations != null ? `${r.violations} violation${r.violations !== 1 ? 's' : ''}` : '';
|
||||
case 'pcell': return Object.entries(r).map(([n, { bytes }]) => `${n}.gds (${bytes} B)`).join(', ');
|
||||
default: return '';
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\nMagic WASM TCL variant — test suite\n');
|
||||
|
||||
const suite = [
|
||||
['extract', runExtract],
|
||||
['gds', runGds],
|
||||
['drc', runDrc],
|
||||
['cif', runCif],
|
||||
['pcell', runPcell],
|
||||
];
|
||||
|
||||
const passed = [];
|
||||
for (const [name, fn] of suite) {
|
||||
passed.push(await test(name, fn));
|
||||
}
|
||||
|
||||
const ok = passed.filter(Boolean).length;
|
||||
console.log(`\n${ok}/${suite.length} passed`);
|
||||
process.exit(ok === suite.length ? 0 : 1);
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
magic::tech load __TECH__
|
||||
magic::load /work/__CELL__
|
||||
magic::cif write /work/__CELL__
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
// cif-tcl.js — CIF export via the TCL variant.
|
||||
import { createMagic, vfsRead, loadCell, loadScript,
|
||||
DEFAULT_TECH, DEFAULT_MAG, DEFAULT_OUT } from './helpers-tcl.js';
|
||||
import { writeFileSync, mkdirSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
export async function run({ magFile = DEFAULT_MAG, tech = DEFAULT_TECH, outputDir = DEFAULT_OUT } = {}) {
|
||||
const { magic } = await createMagic();
|
||||
const { FS } = magic;
|
||||
const { tech: techName, cell } = loadCell(FS, tech, magFile);
|
||||
|
||||
magic.runScript(loadScript('cif-magic.tcl', techName, cell));
|
||||
|
||||
mkdirSync(outputDir, { recursive: true });
|
||||
const data = vfsRead(FS, `/work/${cell}.cif`);
|
||||
if (!data) throw new Error(`CIF export failed: /work/${cell}.cif not created`);
|
||||
|
||||
const outPath = resolve(outputDir, `${cell}.cif`);
|
||||
writeFileSync(outPath, data);
|
||||
return { outPath, bytes: data.length };
|
||||
}
|
||||
|
||||
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
||||
const { outPath, bytes } = await run().catch(e => { console.error(e.message ?? e); process.exit(1); });
|
||||
console.log(`\ncif: ${outPath} (${bytes} bytes)`);
|
||||
console.log('Done.');
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
magic::tech load __TECH__
|
||||
magic::load /work/__CELL__
|
||||
magic::drc catchup
|
||||
magic::drc count total
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
// drc-tcl.js — DRC check via the TCL variant.
|
||||
import { createMagic, loadCell, loadScript,
|
||||
DEFAULT_TECH, DEFAULT_MAG } from './helpers-tcl.js';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
export async function run({ magFile = DEFAULT_MAG, tech = DEFAULT_TECH } = {}) {
|
||||
const output = [];
|
||||
const { magic } = await createMagic({
|
||||
onPrint: msg => { output.push(msg); console.log('[magic-tcl]', msg); },
|
||||
onPrintErr: msg => { output.push(msg); console.error('[magic-tcl]', msg); },
|
||||
});
|
||||
const { FS } = magic;
|
||||
const { tech: techName, cell } = loadCell(FS, tech, magFile);
|
||||
|
||||
magic.runScript(loadScript('drc-magic.tcl', techName, cell));
|
||||
|
||||
const summary = output.find(l => /Total DRC errors/i.test(l));
|
||||
const match = summary?.match(/(\d+)/);
|
||||
const violations = match ? parseInt(match[1], 10) : null;
|
||||
|
||||
return { violations, output };
|
||||
}
|
||||
|
||||
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
||||
const { violations } = await run().catch(e => { console.error(e.message ?? e); process.exit(1); });
|
||||
console.log(`\nDRC violations: ${violations ?? '(count not found in output)'}`);
|
||||
console.log('Done.');
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
magic::tech load __TECH__
|
||||
magic::load /work/__CELL__
|
||||
magic::extract path /work
|
||||
magic::extract do resistance
|
||||
magic::extract all
|
||||
magic::select top cell
|
||||
magic::extresist all
|
||||
magic::ext2spice format ngspice
|
||||
magic::ext2spice extresist on
|
||||
magic::ext2spice cthresh 0
|
||||
magic::ext2spice /work/__CELL__
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
// extract-tcl.js — RC extraction via the TCL variant.
|
||||
import { createMagic, vfsWrite, vfsRead, loadCell, loadScript,
|
||||
DEFAULT_TECH, DEFAULT_MAG, DEFAULT_OUT } from './helpers-tcl.js';
|
||||
import { writeFileSync, mkdirSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
export async function run({ magFile = DEFAULT_MAG, tech = DEFAULT_TECH, outputDir = DEFAULT_OUT } = {}) {
|
||||
const { magic } = await createMagic();
|
||||
const { FS } = magic;
|
||||
const { tech: techName, cell } = loadCell(FS, tech, magFile);
|
||||
|
||||
magic.runScript(loadScript('extract-magic.tcl', techName, cell));
|
||||
|
||||
mkdirSync(outputDir, { recursive: true });
|
||||
|
||||
const extData = vfsRead(FS, `/work/${cell}.ext`);
|
||||
if (!extData) throw new Error(`Extraction failed: /work/${cell}.ext not created`);
|
||||
writeFileSync(resolve(outputDir, `${cell}.ext`), extData);
|
||||
|
||||
const resExtData = vfsRead(FS, `/work/${cell}.res.ext`);
|
||||
if (resExtData) writeFileSync(resolve(outputDir, `${cell}.res.ext`), resExtData);
|
||||
|
||||
const spiceData = vfsRead(FS, `/work/${cell}.spice`) ?? vfsRead(FS, `/work/${cell}.spc`);
|
||||
const spiceExt = vfsRead(FS, `/work/${cell}.spice`) ? 'spice' : 'spc';
|
||||
if (spiceData) writeFileSync(resolve(outputDir, `${cell}.${spiceExt}`), spiceData);
|
||||
|
||||
return {
|
||||
ext: resolve(outputDir, `${cell}.ext`),
|
||||
spice: spiceData ? resolve(outputDir, `${cell}.${spiceExt}`) : null,
|
||||
};
|
||||
}
|
||||
|
||||
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
||||
const { ext, spice } = await run().catch(e => { console.error(e.message ?? e); process.exit(1); });
|
||||
console.log(`\next: ${ext}`);
|
||||
if (spice) console.log(`spice: ${spice}`);
|
||||
console.log('Done.');
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
magic::tech load __TECH__
|
||||
magic::load /work/__CELL__
|
||||
magic::gds write /work/__CELL__
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
// gds-tcl.js — GDS export via the TCL variant.
|
||||
import { createMagic, vfsRead, loadCell, loadScript,
|
||||
DEFAULT_TECH, DEFAULT_MAG, DEFAULT_OUT } from './helpers-tcl.js';
|
||||
import { writeFileSync, mkdirSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
export async function run({ magFile = DEFAULT_MAG, tech = DEFAULT_TECH, outputDir = DEFAULT_OUT } = {}) {
|
||||
const { magic } = await createMagic();
|
||||
const { FS } = magic;
|
||||
const { tech: techName, cell } = loadCell(FS, tech, magFile);
|
||||
|
||||
magic.runScript(loadScript('gds-magic.tcl', techName, cell));
|
||||
|
||||
mkdirSync(outputDir, { recursive: true });
|
||||
const data = vfsRead(FS, `/work/${cell}.gds`);
|
||||
if (!data) throw new Error(`GDS export failed: /work/${cell}.gds not created`);
|
||||
|
||||
const outPath = resolve(outputDir, `${cell}.gds`);
|
||||
writeFileSync(outPath, data);
|
||||
return { outPath, bytes: data.length };
|
||||
}
|
||||
|
||||
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
||||
const { outPath, bytes } = await run().catch(e => { console.error(e.message ?? e); process.exit(1); });
|
||||
console.log(`\ngds: ${outPath} (${bytes} bytes)`);
|
||||
console.log('Done.');
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
// Shared utilities for Magic WASM TCL-variant examples.
|
||||
//
|
||||
// Loads the TCL-enabled WASM variant. Scripts use magic:: prefixed commands;
|
||||
// runScript() evaluates them through the Tcl interpreter via Tcl_EvalFile.
|
||||
import createMagicModule from '../tcl/magic.js';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, resolve, basename } from 'node:path';
|
||||
|
||||
export const EXAMPLES_DIR = dirname(fileURLToPath(import.meta.url));
|
||||
export const wasmBinary = readFileSync(resolve(EXAMPLES_DIR, '../tcl/magic.wasm'));
|
||||
export const DEFAULT_TECH = 'scmos';
|
||||
export const DEFAULT_MAG = resolve(EXAMPLES_DIR, 'min.mag');
|
||||
export const DEFAULT_OUT = resolve(EXAMPLES_DIR, 'output-tcl');
|
||||
|
||||
export { basename };
|
||||
|
||||
export function vfsWrite(FS, vfsPath, source) {
|
||||
const dir = vfsPath.substring(0, vfsPath.lastIndexOf('/'));
|
||||
if (dir) FS.mkdirTree(dir);
|
||||
FS.writeFile(vfsPath, typeof source === 'string' ? readFileSync(source) : source);
|
||||
}
|
||||
|
||||
export function vfsRead(FS, vfsPath) {
|
||||
try { return Buffer.from(FS.readFile(vfsPath)); } catch { return null; }
|
||||
}
|
||||
|
||||
export class MagicWasm {
|
||||
constructor(mod) {
|
||||
this._init = mod.cwrap('magic_wasm_init', 'number', []);
|
||||
this._run = mod.cwrap('magic_wasm_run_command', 'number', ['string']);
|
||||
this._sourceFile = mod.cwrap('magic_wasm_source_file', 'number', ['string']);
|
||||
this.FS = mod.FS;
|
||||
}
|
||||
init() {
|
||||
const rc = this._init();
|
||||
if (rc !== 0) throw new Error(`magic_wasm_init failed with code ${rc}`);
|
||||
}
|
||||
// Evaluate a Tcl script (with magic:: prefixed commands) through the
|
||||
// Tcl interpreter. Writes the script to VFS then calls magic_wasm_source_file,
|
||||
// which in TCL mode uses Tcl_EvalFile.
|
||||
runScript(text) {
|
||||
const path = '/tmp/_magic_script.tcl';
|
||||
this.FS.mkdirTree('/tmp');
|
||||
this.FS.writeFile(path, text);
|
||||
this._sourceFile(path);
|
||||
}
|
||||
// Evaluate a Tcl expression directly (needed for proc definitions in PCell).
|
||||
runTcl(text) {
|
||||
this._run(text);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createMagic({ onPrint, onPrintErr } = {}) {
|
||||
const lines = [];
|
||||
const mod = await createMagicModule({
|
||||
wasmBinary,
|
||||
print: onPrint ?? (msg => { lines.push(msg); console.log('[magic-tcl]', msg); }),
|
||||
printErr: onPrintErr ?? (msg => { lines.push(msg); console.error('[magic-tcl]', msg); }),
|
||||
});
|
||||
const magic = new MagicWasm(mod);
|
||||
magic.init();
|
||||
return { magic, lines };
|
||||
}
|
||||
|
||||
export function loadCell(FS, tech, magFile) {
|
||||
const cell = basename(magFile, '.mag');
|
||||
vfsWrite(FS, `/work/${cell}.mag`, magFile);
|
||||
return { tech, cell };
|
||||
}
|
||||
|
||||
export function loadScript(name, tech, cell) {
|
||||
return readFileSync(resolve(EXAMPLES_DIR, name), 'utf8')
|
||||
.replaceAll('__TECH__', tech)
|
||||
.replaceAll('__CELL__', cell);
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
magic::tech load __TECH__
|
||||
|
||||
proc make_rect {name width height} {
|
||||
magic::cellname create $name
|
||||
magic::box 0 0 $width $height
|
||||
magic::paint m1
|
||||
magic::save /work/$name
|
||||
magic::gds write /work/$name
|
||||
}
|
||||
|
||||
make_rect pcell_4x8 4 8
|
||||
make_rect pcell_8x4 8 4
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
// pcell.js — PCell generation test (TCL variant only).
|
||||
//
|
||||
// Defines a Tcl proc as a PCell, instantiates it with two different sizes,
|
||||
// and verifies that both GDS outputs are non-empty.
|
||||
//
|
||||
// Usage: node examples/pcell.js
|
||||
import { createMagic, vfsRead, loadScript, DEFAULT_TECH, DEFAULT_OUT } from './helpers-tcl.js';
|
||||
import { writeFileSync, mkdirSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
export async function run({ tech = DEFAULT_TECH, outputDir = DEFAULT_OUT } = {}) {
|
||||
const { magic } = await createMagic();
|
||||
const { FS } = magic;
|
||||
|
||||
FS.mkdirTree('/work');
|
||||
magic.runTcl(loadScript('pcell-magic.tcl', tech, ''));
|
||||
|
||||
mkdirSync(outputDir, { recursive: true });
|
||||
|
||||
const cells = ['pcell_4x8', 'pcell_8x4'];
|
||||
const results = {};
|
||||
for (const name of cells) {
|
||||
const data = vfsRead(FS, `/work/${name}.gds`);
|
||||
if (!data || data.length === 0)
|
||||
throw new Error(`PCell GDS output missing or empty: /work/${name}.gds`);
|
||||
const outPath = resolve(outputDir, `${name}.gds`);
|
||||
writeFileSync(outPath, data);
|
||||
results[name] = { outPath, bytes: data.length };
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
||||
const results = await run().catch(e => { console.error(e.message ?? e); process.exit(1); });
|
||||
for (const [name, { outPath, bytes }] of Object.entries(results))
|
||||
console.log(` ${name}.gds: ${outPath} (${bytes} bytes)`);
|
||||
console.log('Done.');
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
# pcell.tcl — PCell generation test.
|
||||
#
|
||||
# Defines a parameterized cell proc and instantiates it with two
|
||||
# different sizes to verify that Tcl proc definitions, Magic drawing
|
||||
# commands, and GDS output all work end-to-end in the TCL variant.
|
||||
#
|
||||
# __TECH__ is substituted by pcell.js before execution.
|
||||
|
||||
tech load __TECH__
|
||||
|
||||
# PCell definition: a labelled metal1 rectangle of variable size.
|
||||
proc make_rect {name width height} {
|
||||
cellname create $name
|
||||
box 0 0 $width $height
|
||||
paint m1
|
||||
save /work/$name
|
||||
gds write /work/$name
|
||||
}
|
||||
|
||||
# Instantiate with two different sizes.
|
||||
make_rect pcell_4x8 4 8
|
||||
make_rect pcell_8x4 8 4
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
"scripts": {
|
||||
"example": "node examples/extract.js",
|
||||
"test": "node examples/all.js",
|
||||
"test:tcl": "node examples/smoke-tcl.mjs",
|
||||
"test:tcl": "node examples/all-tcl.js",
|
||||
"test:gds": "node examples/gds.js",
|
||||
"test:drc": "node examples/drc.js",
|
||||
"test:cif": "node examples/cif.js"
|
||||
|
|
|
|||
Loading…
Reference in New Issue