microsoft/qdk

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
dbwy/random_seed

Branches

Tags

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

Clone

HTTPS

Download ZIP

source/npm/qsharp/test/circuits.js

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