microsoft/qdk

Public

mirrored fromhttps://github.com/microsoft/qdkAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v1.23.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

source/npm/qsharp/test/circuits.js

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