microsoft/qdk

Public

mirrored from https://github.com/microsoft/qdkAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
brlackey/neutral-atom-models

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

source/npm/qsharp/test/circuits.js

313lines · modecode

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT license.
3
4// Circuit snapshot tests: Verifies that Q# circuit diagrams render correctly.
5// To add a new test case, add a .qs or .qsc file to `circuits-cases/` and run with
6// `node --test --test-update-snapshots` or `npm test -- --test-update-snapshots` to generate the snapshot.
7// Snapshots are stored as .html files in `circuits-cases/` and are compared against the rendered output.
8
9// @ts-check
10
11import { JSDOM } from "jsdom";
12import fs, { readFileSync } from "node:fs";
13import path from "node:path";
14import { afterEach, beforeEach, test } from "node:test";
15import { fileURLToPath } from "node:url";
16import prettier from "prettier";
17import { log } from "../dist/log.js";
18import { getCompiler, loadWasmModule } from "../dist/node.js";
19import { draw } from "../dist/ux/circuit-vis/index.js";
20
21// Load the wasm module before running any tests
22const wasmPath = new URL("../lib/web/qsc_wasm_bg.wasm", import.meta.url);
23await loadWasmModule(readFileSync(wasmPath).buffer);
24
25/** @type {import("../dist/log.js").TelemetryEvent[]} */
26const telemetryEvents = [];
27log.setLogLevel("warn");
28log.setTelemetryCollector((event) => telemetryEvents.push(event));
29
30const documentTemplate = `<!doctype html><html>
31 <head>
32 <link rel="stylesheet" href="../../ux/qsharp-ux.css">
33 <link rel="stylesheet" href="../../ux/qsharp-circuit.css">
34 </head>
35 <body>
36 </body>
37</html>`;
38
39/** @type {JSDOM | null} */
40let jsdom = null;
41
42beforeEach(() => {
43 // Create a new test DOM
44 jsdom = new JSDOM(documentTemplate);
45
46 // Override the globals used by product code
47 // @ts-expect-error - the `jsdom` typings and DOM typings don't match
48 globalThis.window = jsdom.window;
49 globalThis.document = jsdom.window.document;
50 globalThis.Node = jsdom.window.Node;
51 globalThis.HTMLElement = jsdom.window.HTMLElement;
52 globalThis.SVGElement = jsdom.window.SVGElement;
53 globalThis.XMLSerializer = jsdom.window.XMLSerializer;
54});
55
56afterEach(() => {
57 jsdom?.window.close();
58 jsdom = null;
59});
60
61/**
62 * Create and add a container div to the document body.
63 * @param {string} id
64 */
65function createContainerElement(id) {
66 const container = document.createElement("div");
67 container.id = id;
68 container.className = "qs-circuit";
69 document.body.appendChild(container);
70 return container;
71}
72
73/**
74 * Walk a directory recursively, yielding file paths.
75 * @param {string} dir
76 * @returns {Iterable<string>}
77 */
78function* walk(dir) {
79 if (fs.existsSync(dir) && fs.statSync(dir).isDirectory()) {
80 for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
81 const full = path.join(dir, entry.name);
82 if (entry.isDirectory()) yield* walk(full);
83 else yield full;
84 }
85 }
86}
87
88/**
89 * Find all files with the given extension under the cases directory.
90 * @param {string} ext
91 * @param {string} dir
92 */
93function findFilesWithExtension(dir, ext) {
94 const candidates = [];
95 for (const f of walk(dir)) {
96 if (f.toLowerCase().endsWith(ext)) candidates.push(f);
97 }
98
99 // Sort for stable test ordering
100 candidates.sort((a, b) => a.localeCompare(b));
101 return candidates;
102}
103
104/**
105 * Get the path to the test cases directory.
106 */
107function getCasesDirectory() {
108 return path.join(
109 path.dirname(fileURLToPath(import.meta.url)),
110 "circuits-cases",
111 );
112}
113
114/**
115 * Get the path to the HTML snapshot for the given test name.
116 * @param {string} name
117 */
118function htmlSnapshotPath(name) {
119 return path.join(getCasesDirectory(), name + ".snapshot.html");
120}
121
122/**
123 * Check the current document against the stored snapshot.
124 * @param {test.TestContext} t
125 * @param {string} name
126 */
127async function checkDocumentSnapshot(t, name) {
128 const rawHtml = new XMLSerializer().serializeToString(document) + "\n";
129
130 // Format with prettier for readable snapshots
131 const formattedHtml = await prettier.format(rawHtml, {
132 parser: "html",
133 printWidth: 80,
134 tabWidth: 2,
135 useTabs: false,
136 });
137
138 t.assert.fileSnapshot(formattedHtml, htmlSnapshotPath(name), {
139 serializers: [(s) => String(s)],
140 });
141}
142
143/**
144 * Load a .qsc JSON file and return the parsed circuit.
145 * @param {string} file
146 * @returns {import("../dist/data-structures/circuit.js").CircuitGroup}
147 */
148function loadCircuit(file) {
149 const raw = fs.readFileSync(file, "utf8");
150 try {
151 return JSON.parse(raw);
152 } catch (e) {
153 throw new Error(
154 `Failed to parse JSON from ${file}: ${/** @type {Error} */ (e).message}`,
155 { cause: e },
156 );
157 }
158}
159
160/**
161 * @param {{ file: string; line: number; column: number; }[]} locations
162 */
163function renderLocations(locations) {
164 let locs = locations.map((loc) => renderLocation(loc));
165 return {
166 title: locs.map((l) => l.title).join("\n"),
167 href: "#",
168 };
169}
170
171/**
172 * @param {{ file: string; line: number; column: number; }} location
173 */
174function renderLocation(location) {
175 // Read the file and extract the specific line
176 try {
177 const filePath = path.join(getCasesDirectory(), location.file);
178 const fileContent = fs.readFileSync(filePath, "utf8");
179 const lines = fileContent.split("\n");
180 const targetLine = lines[location.line] || "";
181 const snippet = targetLine.trim();
182
183 return {
184 title: `${location.file}:${location.line + 1}:${location.column + 1}\n${snippet.replace(/'/g, "\\'")}`,
185 href: "#",
186 };
187 } catch {
188 return {
189 title: `Error loading ${location.file}:${location.line + 1}`,
190 href: "#",
191 };
192 }
193}
194
195test("circuit snapshot tests - .qsc files", async (t) => {
196 const files = findFilesWithExtension(getCasesDirectory(), ".qsc");
197 if (files.length === 0) {
198 t.diagnostic("No .qsc files found under cases");
199 return;
200 }
201
202 for (const file of files) {
203 const relName = path.basename(file);
204 await t.test(relName, async (tt) => {
205 const circuit = loadCircuit(file);
206 const container = createContainerElement(`circuit`);
207 draw(circuit, container, {
208 editor: {
209 editCallback: () => {},
210 },
211 renderLocations,
212 renderDepth: 999999,
213 });
214 await checkDocumentSnapshot(tt, tt.name);
215 });
216 }
217});
218
219test("circuit snapshot tests - .qs files", async (t) => {
220 const files = findFilesWithExtension(getCasesDirectory(), ".qs");
221 if (files.length === 0) {
222 t.diagnostic("No .qs files found under cases");
223 return;
224 }
225
226 for (const file of files) {
227 const relName = path.basename(file);
228 await t.test(`${relName}`, async (tt) => {
229 const circuitSource = fs.readFileSync(file, "utf8");
230 await generateAndDrawCircuit(
231 relName,
232 circuitSource,
233 "circuit-static-collapsed",
234 "static",
235 0,
236 );
237
238 await generateAndDrawCircuit(
239 relName,
240 circuitSource,
241 "circuit-static-expanded",
242 "static",
243 999999,
244 );
245
246 await generateAndDrawCircuit(
247 relName,
248 circuitSource,
249 "circuit-eval-collapsed",
250 "classicalEval",
251 0,
252 );
253
254 await generateAndDrawCircuit(
255 relName,
256 circuitSource,
257 "circuit-eval-expanded",
258 "classicalEval",
259 999999,
260 );
261
262 await checkDocumentSnapshot(tt, tt.name);
263 });
264 }
265});
266
267/**
268 * @param {string} name
269 * @param {string} circuitSource
270 * @param {string} id
271 * @param {"static" | "classicalEval" | "simulate"} generationMethod
272 * @param {number} renderDepth
273 */
274async function generateAndDrawCircuit(
275 name,
276 circuitSource,
277 id,
278 generationMethod,
279 renderDepth,
280) {
281 const compiler = await getCompiler();
282 const title = document.createElement("div");
283 title.innerHTML = `<h2>${id}</h2>`;
284 document.body.appendChild(title);
285 const container = createContainerElement(id);
286 try {
287 // Generate the circuit from Q#
288 const circuit = await compiler.getCircuit(
289 {
290 sources: [[name, circuitSource]],
291 languageFeatures: [],
292 profile: "adaptive_rif",
293 },
294 {
295 generationMethod,
296 groupByScope: true,
297 maxOperations: 100,
298 sourceLocations: true,
299 },
300 undefined,
301 );
302
303 // Render the circuit
304 draw(circuit, container, {
305 renderDepth,
306 renderLocations,
307 });
308 } catch (e) {
309 const pre = document.createElement("pre");
310 pre.appendChild(document.createTextNode(`Error generating circuit: ${e}`));
311 container.appendChild(pre);
312 }
313}
314