From 1f19ac913f66502b52dc33dfad70b8a5fb11860a Mon Sep 17 00:00:00 2001 From: Intubun <41478036+Intubun@users.noreply.github.com> Date: Thu, 21 May 2026 10:25:58 +0200 Subject: [PATCH] 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. --- .github/workflows/main-wasm.yml | 2 +- npm/examples/all-tcl.js | 56 ++++++++++++++++++++++++ npm/examples/cif-magic.tcl | 3 ++ npm/examples/cif-tcl.js | 28 ++++++++++++ npm/examples/drc-magic.tcl | 4 ++ npm/examples/drc-tcl.js | 28 ++++++++++++ npm/examples/extract-magic.tcl | 11 +++++ npm/examples/extract-tcl.js | 39 +++++++++++++++++ npm/examples/gds-magic.tcl | 3 ++ npm/examples/gds-tcl.js | 28 ++++++++++++ npm/examples/helpers-tcl.js | 76 +++++++++++++++++++++++++++++++++ npm/examples/pcell-magic.tcl | 12 ++++++ npm/examples/pcell.js | 39 +++++++++++++++++ npm/examples/pcell.tcl | 22 ++++++++++ npm/package.json | 2 +- 15 files changed, 351 insertions(+), 2 deletions(-) create mode 100644 npm/examples/all-tcl.js create mode 100644 npm/examples/cif-magic.tcl create mode 100644 npm/examples/cif-tcl.js create mode 100644 npm/examples/drc-magic.tcl create mode 100644 npm/examples/drc-tcl.js create mode 100644 npm/examples/extract-magic.tcl create mode 100644 npm/examples/extract-tcl.js create mode 100644 npm/examples/gds-magic.tcl create mode 100644 npm/examples/gds-tcl.js create mode 100644 npm/examples/helpers-tcl.js create mode 100644 npm/examples/pcell-magic.tcl create mode 100644 npm/examples/pcell.js create mode 100644 npm/examples/pcell.tcl diff --git a/.github/workflows/main-wasm.yml b/.github/workflows/main-wasm.yml index ff8fe65b..c5282a82 100644 --- a/.github/workflows/main-wasm.yml +++ b/.github/workflows/main-wasm.yml @@ -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 diff --git a/npm/examples/all-tcl.js b/npm/examples/all-tcl.js new file mode 100644 index 00000000..20ec5c12 --- /dev/null +++ b/npm/examples/all-tcl.js @@ -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); diff --git a/npm/examples/cif-magic.tcl b/npm/examples/cif-magic.tcl new file mode 100644 index 00000000..695975f6 --- /dev/null +++ b/npm/examples/cif-magic.tcl @@ -0,0 +1,3 @@ +magic::tech load __TECH__ +magic::load /work/__CELL__ +magic::cif write /work/__CELL__ diff --git a/npm/examples/cif-tcl.js b/npm/examples/cif-tcl.js new file mode 100644 index 00000000..b7e0c031 --- /dev/null +++ b/npm/examples/cif-tcl.js @@ -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.'); +} diff --git a/npm/examples/drc-magic.tcl b/npm/examples/drc-magic.tcl new file mode 100644 index 00000000..84b951e5 --- /dev/null +++ b/npm/examples/drc-magic.tcl @@ -0,0 +1,4 @@ +magic::tech load __TECH__ +magic::load /work/__CELL__ +magic::drc catchup +magic::drc count total diff --git a/npm/examples/drc-tcl.js b/npm/examples/drc-tcl.js new file mode 100644 index 00000000..eb7889a9 --- /dev/null +++ b/npm/examples/drc-tcl.js @@ -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.'); +} diff --git a/npm/examples/extract-magic.tcl b/npm/examples/extract-magic.tcl new file mode 100644 index 00000000..08f15e2a --- /dev/null +++ b/npm/examples/extract-magic.tcl @@ -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__ diff --git a/npm/examples/extract-tcl.js b/npm/examples/extract-tcl.js new file mode 100644 index 00000000..45b5b0c3 --- /dev/null +++ b/npm/examples/extract-tcl.js @@ -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.'); +} diff --git a/npm/examples/gds-magic.tcl b/npm/examples/gds-magic.tcl new file mode 100644 index 00000000..38bce814 --- /dev/null +++ b/npm/examples/gds-magic.tcl @@ -0,0 +1,3 @@ +magic::tech load __TECH__ +magic::load /work/__CELL__ +magic::gds write /work/__CELL__ diff --git a/npm/examples/gds-tcl.js b/npm/examples/gds-tcl.js new file mode 100644 index 00000000..a1379442 --- /dev/null +++ b/npm/examples/gds-tcl.js @@ -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.'); +} diff --git a/npm/examples/helpers-tcl.js b/npm/examples/helpers-tcl.js new file mode 100644 index 00000000..35c12398 --- /dev/null +++ b/npm/examples/helpers-tcl.js @@ -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); +} diff --git a/npm/examples/pcell-magic.tcl b/npm/examples/pcell-magic.tcl new file mode 100644 index 00000000..13ecac5d --- /dev/null +++ b/npm/examples/pcell-magic.tcl @@ -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 diff --git a/npm/examples/pcell.js b/npm/examples/pcell.js new file mode 100644 index 00000000..95c94ac5 --- /dev/null +++ b/npm/examples/pcell.js @@ -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.'); +} diff --git a/npm/examples/pcell.tcl b/npm/examples/pcell.tcl new file mode 100644 index 00000000..91e5ad4b --- /dev/null +++ b/npm/examples/pcell.tcl @@ -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 diff --git a/npm/package.json b/npm/package.json index e10338dc..8667a4d5 100644 --- a/npm/package.json +++ b/npm/package.json @@ -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"