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
5801acce62
commit
7c71d0f3e5
|
|
@ -97,7 +97,7 @@ jobs:
|
||||||
- name: Run example tests (non-TCL variant)
|
- name: Run example tests (non-TCL variant)
|
||||||
run: cd npm && npm test
|
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
|
run: cd npm && npm run test:tcl
|
||||||
|
|
||||||
# Dump generated text outputs (.ext, .spice, .cif, …) into the CI log
|
# 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": {
|
"scripts": {
|
||||||
"example": "node examples/extract.js",
|
"example": "node examples/extract.js",
|
||||||
"test": "node examples/all.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:gds": "node examples/gds.js",
|
||||||
"test:drc": "node examples/drc.js",
|
"test:drc": "node examples/drc.js",
|
||||||
"test:cif": "node examples/cif.js"
|
"test:cif": "node examples/cif.js"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue