147 lines
6.3 KiB
JavaScript
147 lines
6.3 KiB
JavaScript
/*
|
|
* Description: Static and helper regression tests for MXPIC EDA frontend/backend integration contracts.
|
|
* Inside functions: N/A - assertion-based test/module script.
|
|
* Developer : Qin Yue @ 2026
|
|
* Organization : OptiHK Limited
|
|
*/
|
|
const assert = require('assert');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const root = path.resolve(__dirname, '..');
|
|
const backendDir = path.join(root, 'backend');
|
|
const serverPy = fs.readFileSync(path.join(backendDir, 'server.py'), 'utf8');
|
|
|
|
assert(
|
|
fs.existsSync(path.join(backendDir, 'layout_preview.py')),
|
|
'backend/layout_preview.py should generate SVG previews from saved layout YAML'
|
|
);
|
|
assert(
|
|
fs.existsSync(path.join(backendDir, 'pdk_registry.py')),
|
|
'backend/pdk_registry.py should resolve public PDK YAML/GDS assets'
|
|
);
|
|
assert(
|
|
fs.existsSync(path.join(backendDir, 'gds_builder.py')),
|
|
'backend/gds_builder.py should build hierarchical GDS from saved project YAML'
|
|
);
|
|
assert(
|
|
fs.existsSync(path.join(backendDir, 'routed_layout_preview.py')),
|
|
'backend/routed_layout_preview.py should create routed SVG previews through mxpic_router'
|
|
);
|
|
assert(
|
|
serverPy.includes('create_layout_svg_from_gds'),
|
|
'save-layout route should create a GDS-derived layout SVG preview'
|
|
);
|
|
assert(
|
|
serverPy.includes('create_routed_layout_svg'),
|
|
'save-layout route should use routed preview generation when links exist'
|
|
);
|
|
assert(
|
|
serverPy.includes('cell_routes_path') &&
|
|
serverPy.includes('write_route_points_sidecar') &&
|
|
serverPy.includes('.routes.yml'),
|
|
'save-layout should write a sidecar route-points file for manually drawn link guidance'
|
|
);
|
|
assert(
|
|
serverPy.includes('svg_url'),
|
|
'save-layout response should include an svg_url for the new layout tab'
|
|
);
|
|
assert(
|
|
serverPy.includes("@app.route('/api/projects/<project_name>/cells/<cell_name>/layout.svg')"),
|
|
'server should expose a route for saved cell SVG previews'
|
|
);
|
|
assert(
|
|
serverPy.includes("@app.route('/api/build-gds'"),
|
|
'server should expose a Build GDS API route'
|
|
);
|
|
assert(
|
|
serverPy.includes("@app.route('/api/technologies/<foundry>/<technology>/manifest'"),
|
|
'server should expose a technology manifest API route'
|
|
);
|
|
assert(
|
|
fs.existsSync(path.join(backendDir, 'technology_manifest.py')),
|
|
'backend/technology_manifest.py should read generated technology manifests'
|
|
);
|
|
|
|
const techManifestPath = path.join(root, 'mxpic', 'PDKs', 'Silterra', 'EMO1_2ML_CU_Al_RDL', 'technology.yml');
|
|
assert(
|
|
fs.existsSync(techManifestPath),
|
|
'Silterra technology.yml should be generated into the EDA PDK folder'
|
|
);
|
|
const techManifest = fs.readFileSync(techManifestPath, 'utf8');
|
|
for (const xsection of ['strip', 'rib_low', 'metal_1', 'metal_2']) {
|
|
assert(techManifest.includes(`${xsection}:`), `technology.yml should include ${xsection}`);
|
|
}
|
|
assert(techManifest.includes('family: optical'), 'technology.yml should classify optical xsections');
|
|
assert(techManifest.includes('family: electrical'), 'technology.yml should classify electrical xsections');
|
|
|
|
const layoutPreviewPy = fs.readFileSync(path.join(backendDir, 'layout_preview.py'), 'utf8');
|
|
assert(
|
|
layoutPreviewPy.includes('read_gds') || layoutPreviewPy.includes('load_gds'),
|
|
'layout_preview.py should load public _BB.gds geometry, not draw only schematic boxes'
|
|
);
|
|
assert(
|
|
layoutPreviewPy.includes('_BB.gds') || layoutPreviewPy.includes('gds_path'),
|
|
'layout_preview.py should resolve public GDS assets for placed components'
|
|
);
|
|
|
|
const gdsBuilderPy = fs.readFileSync(path.join(backendDir, 'gds_builder.py'), 'utf8');
|
|
assert(
|
|
gdsBuilderPy.includes('_cells_have_links') && gdsBuilderPy.includes('Routed Build GDS requires mxpic_router'),
|
|
'Build GDS should not silently fall back to unrouted gdstk when links are present'
|
|
);
|
|
assert(
|
|
gdsBuilderPy.includes('_cells_have_elements') &&
|
|
gdsBuilderPy.includes('Build GDS with Port/Anchor elements requires Nazca') &&
|
|
gdsBuilderPy.indexOf('if _cells_have_elements(cells):') < gdsBuilderPy.indexOf('return _build_with_gdstk'),
|
|
'Build GDS should route layouts with Port/Anchor elements through Nazca so element pins survive as cell metadata'
|
|
);
|
|
assert(
|
|
gdsBuilderPy.includes('_build_nazca_element_cells') &&
|
|
gdsBuilderPy.includes('_build_nazca_element_cell') &&
|
|
gdsBuilderPy.includes('element.get("pin_number"') &&
|
|
gdsBuilderPy.includes('_element_pin_names') &&
|
|
gdsBuilderPy.includes('nd.Pin(pin_name, width=width).put(0.0, y, 180.0)') &&
|
|
gdsBuilderPy.includes('nd.Pin(anchor_pin_names[index * 2], width=width).put(0.0, y, 180.0)') &&
|
|
gdsBuilderPy.includes('nd.Pin(anchor_pin_names[index * 2 + 1], width=width).put(0.0, y, 0.0)') &&
|
|
gdsBuilderPy.includes('element_cell.put(x, y, rotation)'),
|
|
'Nazca fallback should model Port and Anchor objects as placed element cells with named local nd.Pin definitions'
|
|
);
|
|
|
|
const routerDir = path.resolve(root, '..', 'mxpic_router', 'mxpic_router');
|
|
if (fs.existsSync(routerDir)) {
|
|
const routerLoaderPy = fs.readFileSync(path.join(routerDir, 'eda_loader.py'), 'utf8');
|
|
const routerBuilderPy = fs.readFileSync(path.join(routerDir, 'builder.py'), 'utf8');
|
|
assert(
|
|
routerLoaderPy.includes('pins: Dict[str, PinSpec]') &&
|
|
routerLoaderPy.includes('pin_number') &&
|
|
routerLoaderPy.includes('pitch: float = 10.0') &&
|
|
routerLoaderPy.includes('pins=_pins(element.get("pins"))'),
|
|
'mxpic_router loader should parse pins-only layout metadata from exported elements'
|
|
);
|
|
assert(
|
|
routerBuilderPy.includes('_port_element_pin_entries') &&
|
|
routerBuilderPy.includes('_anchor_element_pin_entries') &&
|
|
routerBuilderPy.includes('_metadata_pins') &&
|
|
routerBuilderPy.includes('link.src_pin'),
|
|
'mxpic_router builder should register named element pins and route through pin endpoints'
|
|
);
|
|
}
|
|
|
|
assert(
|
|
serverPy.includes('def scoped_pdk_root_for_project') &&
|
|
serverPy.includes('read_project_meta(project_name).get("technology")') &&
|
|
serverPy.includes('os.path.join(base_root, foundry, technology)'),
|
|
'backend should resolve a project-scoped PDK root from selected foundry/technology'
|
|
);
|
|
assert(
|
|
serverPy.includes('request.args.get(\'project\')') &&
|
|
serverPy.includes('scoped_pdk_root_for_project(project)'),
|
|
'library/component APIs should accept ?project= and search inside the selected technology folder'
|
|
);
|
|
assert(
|
|
serverPy.includes('__path__') &&
|
|
serverPy.includes('os.path.relpath(root, path_root)'),
|
|
'library tree leaves should preserve component paths relative to the role PDK root'
|
|
);
|