microsoft/qdk

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
minestarks/circuit-source-links-v2

Branches

Tags

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

Clone

HTTPS

Download ZIP

source/npm/qsharp/test/circuits.js

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