qiskit-documentation/scripts/lib/api/TocGrouping.test.ts

170 lines
5.8 KiB
TypeScript

// 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, readdir } from "fs/promises";
import { expect, describe, 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<ModuleGroup[]> {
const rawIndex = await readFile(fp, "utf-8");
const result: ModuleGroup[] = [];
let currentGroup: ModuleGroup = [];
for (const line of rawIndex.split("\n")) {
// Each ModuleGroup represents an unordered list of entries starting with `*`.
// So, when we stop encountering `*`, we need to start a new ModuleGroup.
if (!line.startsWith("* ")) {
if (currentGroup.length) {
result.push(currentGroup);
currentGroup = [];
}
continue;
}
// Certain classes like QuantumCircuit in Qiskit 1.1+ have manually
// created pages. Those pages show up in index.mdx as top-level entries,
// but they are not top-level entries in the left ToC. This is expected.
// So, we allow the index to diverge from the left ToC.
//
// This is looking for e.g. '[`QuantumCircuit` class](qiskit.circuit.QuantumCircuit)'
const isDedicatedClassPage = line.includes(" class](");
if (isDedicatedClassPage) continue;
const module = extractModuleName(line);
currentGroup.push(module);
}
return result;
}
async function getTocModuleGroups(fp: string): Promise<ModuleGroup[]> {
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;
}
async function checkFolder(dirName: string): Promise<void> {
const indexModuleGroups = await getIndexModuleGroups(
`docs/api/qiskit${dirName}/index.mdx`,
);
const tocModuleGroups = await getTocModuleGroups(
`docs/api/qiskit${dirName}/_toc.json`,
);
expect(indexModuleGroups).toEqual(tocModuleGroups);
}
describe("Qiskit ToC mirrors index page sections", () => {
test("validate assumptions", () => {
validateTopLevelModuleAssumptions();
});
test("dev", async () => {
await checkFolder("/dev");
});
test("latest", async () => {
await checkFolder("");
});
test("historical releases (1.1+)", async () => {
const folders = (
await readdir("docs/api/qiskit", { withFileTypes: true })
).filter(
(file) =>
file.isDirectory() && file.name.match(/[0-9].*/) && +file.name >= 1.1,
);
for (const folder of folders) {
await checkFolder(`/${folder.name}`);
}
});
});