197 lines
8.2 KiB
JavaScript
197 lines
8.2 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');
|
|
const removedInternalPdkPath = ['mxpic', 'PDKs'].join('/');
|
|
const removedEdaPdkRootName = 'EDA_' + 'PDK_ROOT';
|
|
const removedYmlPathName = 'YML_' + 'PATH';
|
|
|
|
assert(
|
|
!fs.existsSync(path.join(backendDir, 'layout_preview.py')),
|
|
'EDA backend should not keep the legacy local GDS/SVG preview builder'
|
|
);
|
|
assert(
|
|
!fs.existsSync(path.join(backendDir, 'pdk_registry.py')),
|
|
'EDA backend should not keep the legacy local PDK GDS registry'
|
|
);
|
|
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(
|
|
fs.existsSync(path.join(backendDir, 'router_dependency.py')),
|
|
'backend/router_dependency.py should validate build-time mxpic_router runtime dependencies'
|
|
);
|
|
assert(
|
|
!serverPy.includes('from router_dependency import require_router_stack') &&
|
|
!serverPy.includes('Verified mxpic_router stack') &&
|
|
!serverPy.includes('require_router_stack()'),
|
|
'server startup should not require mxpic_router; only build actions should validate the router stack'
|
|
);
|
|
assert(
|
|
!serverPy.includes('create_layout_svg_from_gds'),
|
|
'save-layout route should not use the legacy gdstk-only preview builder'
|
|
);
|
|
assert(
|
|
serverPy.includes('create_routed_layout_svg') &&
|
|
!serverPy.includes('if layout_has_links(content):'),
|
|
'save-layout route should always use routed preview generation through mxpic_router'
|
|
);
|
|
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(
|
|
<<<<<<< HEAD
|
|
serverPy.includes('svg_ready') &&
|
|
serverPy.includes('svg_version') &&
|
|
serverPy.includes('file_version(svg_path)') &&
|
|
serverPy.includes("url_for('get_layout_svg', project_name=project, cell_name=cell, v=svg_version)"),
|
|
'save-layout response should only expose a versioned SVG URL after the preview file is ready'
|
|
);
|
|
assert(
|
|
serverPy.includes('temp_svg_path') &&
|
|
serverPy.includes('os.replace(temp_svg_path, svg_path)'),
|
|
'save-layout should publish generated SVG previews atomically instead of serving partially written files'
|
|
);
|
|
assert(
|
|
=======
|
|
>>>>>>> jingwen_main
|
|
serverPy.includes('RouterStackUnavailable') &&
|
|
serverPy.includes('except RouterStackUnavailable as e') &&
|
|
serverPy.includes('"preview_status": preview_status') &&
|
|
serverPy.includes('"preview_error": preview_error'),
|
|
'save-layout should still return success and explain skipped SVG preview when mxpic_router is unavailable'
|
|
);
|
|
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'
|
|
);
|
|
|
|
assert(
|
|
!serverPy.includes(removedEdaPdkRootName) &&
|
|
!serverPy.includes(removedYmlPathName) &&
|
|
!serverPy.includes("'mxpic', 'PDKs'") &&
|
|
!serverPy.includes(removedInternalPdkPath),
|
|
'server should not use an internal EDA PDK copy as a separate technology source of truth'
|
|
);
|
|
assert(
|
|
serverPy.includes('pdks_root = current_pdk_root()') &&
|
|
serverPy.includes('os.path.exists(os.path.join(technology_path, "technology.yml"))'),
|
|
'technology list should scan the active role PDK root and only expose folders containing technology.yml'
|
|
);
|
|
assert(
|
|
serverPy.includes('read_technology_manifest(current_pdk_root()'),
|
|
'technology manifest API should read technology.yml from the active role PDK root'
|
|
);
|
|
assert(
|
|
!fs.existsSync(path.join(root, 'mxpic')),
|
|
'mxpic_EDA should not contain a redundant internal technology copy'
|
|
);
|
|
|
|
const gdsBuilderPy = fs.readFileSync(path.join(backendDir, 'gds_builder.py'), 'utf8');
|
|
assert(
|
|
gdsBuilderPy.includes('return _build_with_mxpic_router(') &&
|
|
!gdsBuilderPy.includes('return _build_with_gdstk') &&
|
|
!gdsBuilderPy.includes('return _build_with_nazca'),
|
|
'Build GDS should use mxpic_router as the only production builder'
|
|
);
|
|
assert(
|
|
gdsBuilderPy.includes('from router_dependency import require_router_stack') &&
|
|
gdsBuilderPy.includes('require_router_stack()'),
|
|
'Build GDS API wrapper should validate mxpic_router only when a GDS build is requested'
|
|
);
|
|
|
|
const routedPreviewPy = fs.readFileSync(path.join(backendDir, 'routed_layout_preview.py'), 'utf8');
|
|
assert(
|
|
routedPreviewPy.includes('from router_dependency import require_router_stack') &&
|
|
routedPreviewPy.includes('require_router_stack(require_gdstk=True)'),
|
|
'Build Layout preview should validate mxpic_router and gdstk only when preview generation is requested'
|
|
);
|
|
|
|
const routerDependencyPy = fs.readFileSync(path.join(backendDir, 'router_dependency.py'), 'utf8');
|
|
assert(
|
|
routerDependencyPy.includes('def require_router_stack') &&
|
|
routerDependencyPy.includes('class RouterStackUnavailable') &&
|
|
routerDependencyPy.includes('require_gdstk: bool = False') &&
|
|
routerDependencyPy.includes('importlib.import_module("nazca")') &&
|
|
routerDependencyPy.includes('mxpic_router.builder') &&
|
|
routerDependencyPy.includes('_import_route_backend'),
|
|
'router dependency gate should validate mxpic_router, Nazca, optional gdstk, and route backend imports'
|
|
);
|
|
|
|
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') &&
|
|
routerBuilderPy.includes('_NazcaInterconnectRoute') &&
|
|
routerBuilderPy.includes('_import_route_backend'),
|
|
'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'
|
|
);
|
|
assert(
|
|
serverPy.includes('f.lower() != "technology.yml"') &&
|
|
serverPy.includes('component_yml_files'),
|
|
'component library scanning should ignore technology.yml so it can descend into technology folders'
|
|
);
|