diff --git a/scripts/lib/api/TocGrouping.test.ts b/scripts/lib/api/TocGrouping.test.ts new file mode 100644 index 00000000000..6c1dd25a902 --- /dev/null +++ b/scripts/lib/api/TocGrouping.test.ts @@ -0,0 +1,133 @@ +// This code is a Qiskit project. +// +// (C) Copyright IBM 2024. +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +import { readFile } from "fs/promises"; + +import { expect, test } from "@jest/globals"; + +import { QISKIT_TOC_GROUPING } from "./TocGrouping"; +import type { TocEntry } from "./generateToc"; + +/** + * The module names belonging to a section, e.g. + * `['qiskit.circuit', 'qiskit.circuit.library']`. + * + * For top-level modules, like `qiskit.quantum_info`, there will only + * be a single element. + * */ +type ModuleGroup = string[]; + +/** + * Ensure our assumptions about Qiskit's TocGrouping are correct. + * + * These assumptions are what allow us to infer what is a top-level module + * (like 'qiskit.quantum_info') vs. a section (like 'Circuit construction'). + * + * If these assumptions are getting in the way, you can rewrite these tests. + * A more robust approach would be to read the front-matter/metadata for the + * URLs to see if `python_api_type: module` is set. This is complicated by + * module pages sometimes being in 'Module overview' vs being on a standalone page + * like 'qiskit.circuit.singleton'. + */ +function validateTopLevelModuleAssumptions(): void { + for (const entry of QISKIT_TOC_GROUPING.entries) { + if ( + entry.kind === "module" && + !entry.title.includes("qiskit.") && + entry.title !== "API index" + ) { + throw new Error( + "Expected every top-level module of QISKIT_TOC_GROUPING to have the module name in " + + "its title, e.g. 'Quantum information (qiskit.quantum_info)'. This will break the " + + "tests in this file. Either add the module name to the title or rewrite these tests. " + + `Bad entry: ${entry.title}`, + ); + } else if (entry.kind === "section" && entry.name.includes("qiskit.")) { + throw new Error( + "Expected every `section` of QISKIT_TOC_GROUPING.entries to not have 'qiskit.' in the " + + "name. This will break the tests in this file. Either remove the module name or " + + `rewrite these tests. Bad entry: ${entry.name}`, + ); + } + } +} + +function extractModuleName(text: string): string { + const re = /qiskit\.[a-zA-Z._0-9]+/; + // Ex: 'Quantum information (qiskit.quantum_info)'. + // Ex: '* [Quantum Circuits (`qiskit.circuit`)](circuit)' + const match = text.match(re); + if (!match) { + throw new Error(`Could not extract module from ${text}`); + } + return match[0]; +} + +/** + * Finds all groups of modules from the index page. + * + * Each group has a list of page titles with the module name in parantheses. + */ +async function getIndexModuleGroups(fp: string): Promise { + const rawIndex = await readFile(fp, "utf-8"); + const result: ModuleGroup[] = []; + let currentGroup: ModuleGroup = []; + for (const line of rawIndex.split("\n")) { + if (line.startsWith("* ")) { + if (line.includes("qiskit.")) { + const module = extractModuleName(line); + currentGroup.push(module); + } + continue; + } else if (currentGroup.length) { + result.push(currentGroup); + currentGroup = []; + } + } + return result; +} + +async function getTocModuleGroups(fp: string): Promise { + const rawToc = await readFile(fp, "utf-8"); + const entries = JSON.parse(rawToc).children as TocEntry[]; + const result: ModuleGroup[] = []; + for (const entry of entries) { + const isTopLevelModule = entry.title.includes("qiskit."); + if (isTopLevelModule) { + const moduleName = extractModuleName(entry.title); + result.push([moduleName]); + } else if (entry.children) { + // The modules inside a custom Section cannot be renamed, so they + // will have their title set as the module, e.g. `qiskit.circuit`. + const childrenModules = Array.from( + entry.children + .filter((child) => child.title.startsWith("qiskit.")) + .map((child) => child.title), + ); + if (childrenModules.length) { + result.push(childrenModules); + } + } + } + return result; +} + +test("Qiskit ToC mirrors index page sections", async () => { + validateTopLevelModuleAssumptions(); + const indexModuleGroups = await getIndexModuleGroups( + "docs/api/qiskit/dev/index.mdx", + ); + const tocModuleGroups = await getTocModuleGroups( + "docs/api/qiskit/dev/_toc.json", + ); + expect(indexModuleGroups).toEqual(tocModuleGroups); +});