VSCodium / VS Code extension which lets you run OCaml PPX Expect and PPX Inline Test with the (native) Test Explorer UI.
1/*
2 * SPDX-License-Identifier: MIT
3 * Copyright (C) 2023 Roland Csaszar
4 *
5 * Project: vscode-ocaml-expect-inline
6 * File: parse_source.ts
7 * Date: 04.Mar.2023
8 *
9 * ==============================================================================
10 * Parse a single source file for test definitions.
11 */
12
13import * as c from "./constants";
14import * as h from "./extension_helpers";
15import * as io from "./osInteraction";
16import * as p from "./parsing";
17import * as t from "./list_tests";
18import * as vscode from "vscode";
19
20/**
21 * Parse the given source file for tests and add them to the Test Explorer's
22 * tree.
23 * @param env The extension's environment.
24 * @param source The source file to parse for tests.
25 */
26// eslint-disable-next-line max-lines-per-function, max-statements
27export async function parseTextDocument(
28 env: h.Env,
29 source: vscode.TextDocument
30) {
31 const relPath = h.toRelativePath(source.uri) as {
32 root: vscode.WorkspaceFolder;
33 path: string;
34 };
35 const sanitizedTests = p
36 .parseTextForTests(source.getText())
37 .map(({ name, range }) => ({
38 name:
39 name === "_"
40 ? h.toTestName(relPath.path, range.start.line + 1)
41 : name,
42 range,
43 }));
44
45 const parents = io.getListParentDirs(relPath.path);
46 for (const parent of parents) {
47 if (
48 // eslint-disable-next-line no-await-in-loop
49 await hasAddedTests(env, {
50 relPath,
51 parent,
52 sanitizedTests,
53 source,
54 })
55 ) {
56 return;
57 }
58 }
59}
60
61/**
62 * Return `true` if we have found a dune file containing a library definition
63 * and the tests have been added to the Test Explorer's tree.
64 * @param env The extension's environment.
65 * @param data The needed data.
66 * @returns `true` if we have found a dune file containing a library definition.
67 * `false` else.
68 */
69async function hasAddedTests(
70 env: h.Env,
71 data: {
72 relPath: { root: vscode.WorkspaceFolder; path: string };
73 parent: string;
74 sanitizedTests: {
75 name: string;
76 range: vscode.Range;
77 }[];
78 source: vscode.TextDocument;
79 }
80) {
81 const duneFile = vscode.Uri.joinPath(
82 data.relPath.root.uri,
83 data.parent.concat("/" + c.duneFileName)
84 );
85 if (await io.existsIsFile(duneFile)) {
86 const bytes = await vscode.workspace.fs.readFile(duneFile);
87 const libName = p.parseDuneLib(bytes.toString());
88 if (libName) {
89 env.outChannel.appendLine(
90 `Found library "${libName}" in dune file ${duneFile.path}`
91 );
92 await addTests(env, {
93 relPath: data.relPath,
94 parent: data.parent,
95 sanitizedTests: data.sanitizedTests,
96 source: data.source,
97 libName,
98 });
99 return true;
100 }
101 }
102 return false;
103}
104
105/**
106 * Add all tests to the Test Explorer's tree.
107 * @param env The extension's environment.
108 * @param data The needed data.
109 */
110async function addTests(
111 env: h.Env,
112 data: {
113 relPath: { root: vscode.WorkspaceFolder; path: string };
114 parent: string;
115 sanitizedTests: {
116 name: string;
117 range: vscode.Range;
118 }[];
119 source: vscode.TextDocument;
120 libName: string;
121 }
122) {
123 const { groupItem } = getOrCreateParents(env, data.relPath);
124 // eslint-disable-next-line no-await-in-loop
125 const out = await io.runDuneBuild(undefined, data.relPath.root, {
126 duneCmd: c.getCfgDunePath(env.config),
127 libDir: data.parent,
128 libName: data.libName,
129 });
130 env.outChannel.appendLine(
131 `Dune build output stdout: ${out.stdout} stderr: ${out.stderr} error: ${
132 out.error ? out.error : ""
133 }`
134 );
135
136 data.sanitizedTests.forEach((test) =>
137 t.addTestItem(env, {
138 t: {
139 name: test.name,
140 line: test.range.start.line + 1,
141 startCol: test.range.start.character,
142 endCol: test.range.end.character,
143 },
144 root: data.relPath.root,
145 sourcePath: data.source.uri,
146 groupItem,
147 runnerPath: c.fullRunnerPath(data.parent, data.libName),
148 })
149 );
150}
151
152/**
153 * Return the two top nodes of the Test Explorer tree:
154 * `{ workspaceItem, groupItem }`.
155 * If they do not exist, create them.
156 * @param env The extension's environment.
157 * @param relPath The relative path to the source file.
158 * @returns The two top nodes of the Test Explorer tree:
159 * `{ workspaceItem, groupItem }`.
160 */
161function getOrCreateParents(
162 env: h.Env,
163 relPath: { root: vscode.WorkspaceFolder; path: string }
164) {
165 const workspaceItem = getOrCreateItem(env, {
166 items: env.controller.items,
167 id: relPath.root.name,
168 label: c.workspaceLabel(relPath.root.name),
169 uri: relPath.root.uri,
170 delete: false,
171 });
172 const groupItem = getOrCreateItem(env, {
173 items: workspaceItem.children,
174 id: relPath.path,
175 label: relPath.path,
176 uri: vscode.Uri.joinPath(relPath.root.uri, relPath.path),
177 delete: true,
178 });
179 return { workspaceItem, groupItem };
180}
181
182/**
183 * Either return an existing `TestItem` with the given data or create one and
184 * add it to the Test Explorer.
185 * If `data.delete` is set, an existing `TestItem` and all of its children are
186 * being deleted and created.
187 * @param env The extension's environment.
188 * @param data The needed data.
189 * @returns The created or existing `TestItem`.
190 */
191function getOrCreateItem(
192 env: h.Env,
193 data: {
194 items: vscode.TestItemCollection;
195 id: string;
196 label: string;
197 uri: vscode.Uri;
198 delete: boolean;
199 }
200) {
201 let testItem = data.items.get(data.id);
202 if (!testItem) {
203 testItem = env.controller.createTestItem(data.id, data.label, data.uri);
204 data.items.add(testItem);
205 } else if (data.delete) {
206 testItem.children.forEach((i) => {
207 env.outChannel.appendLine(
208 `Deleting "${i.label}" Line: ${parseInt(i.id, 10) + 1}`
209 );
210 testItem?.children.delete(i.id);
211 });
212 data.items.delete(data.id);
213 testItem = env.controller.createTestItem(data.id, data.label, data.uri);
214 data.items.add(testItem);
215 }
216
217 return testItem;
218}