microsoft/qdk

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
iadavis/pipeline-issue-debugging

Branches

Tags

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

Clone

HTTPS

Download ZIP

source/npm/qsharp/test/circuits.js

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