VSCodium / VS Code extension which lets you run OCaml PPX Expect and PPX Inline Test with the (native) Test Explorer UI.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 419 lines 13 kB view raw
1/* 2 * SPDX-License-Identifier: MIT 3 * Copyright (C) 2023 Roland Csaszar 4 * 5 * Project: vscode-ocaml-expect-inline 6 * File: list_tests.ts 7 * Date: 28.Feb.2023 8 * 9 * ============================================================================== 10 * Everything to add tests to the Test Explorer tree from the output of an 11 * inline runner list of tests. 12 */ 13import * as c from "./constants"; 14import * as h from "./extension_helpers"; 15import * as io from "./osInteraction"; 16import * as p from "./parsing"; 17import * as vscode from "vscode"; 18 19/** 20 * Add all tests of all workspaces to the Test Explorer. 21 * @param env The extension's environment. 22 * @returns The list of `TestItems` that have been deleted from the Test 23 * Explorer tree. 24 */ 25export async function addTests( 26 env: h.Env, 27 roots: readonly vscode.WorkspaceFolder[] 28) { 29 env.outChannel.appendLine("Adding new tests ..."); 30 31 const promises = []; 32 for (const root of roots) { 33 env.outChannel.appendLine(`Processing workspace ${root.name}`); 34 promises.push(addWorkspaceTests(env, root)); 35 } 36 37 const toDeleteArray = await Promise.allSettled(promises); 38 39 env.outChannel.appendLine("Finished adding new tests."); 40 41 // eslint-disable-next-line arrow-body-style 42 return toDeleteArray.flatMap((e) => { 43 return e.status === "fulfilled" ? e.value : []; 44 }); 45} 46 47/** 48 * Add all tests of a single workspace `root` to the Test Explorer. 49 * @param env Everything needed to add these tests. 50 * @param root The workspace to add the tests from. 51 * @returns The list of `TestItems` that have been deleted from the Test 52 * Explorer tree. 53 */ 54// eslint-disable-next-line max-statements 55async function addWorkspaceTests(env: h.Env, root: vscode.WorkspaceFolder) { 56 await setOpamEnv(env, root); 57 58 // eslint-disable-next-line @typescript-eslint/no-extra-parens 59 if (!(await h.isDuneWorking(root, env))) { 60 vscode.window.showErrorMessage( 61 `Error: Dune command '${c.getCfgDunePath( 62 env.config 63 )}' is not working in ${ 64 root.name 65 }.\nSee the 'Output' window view of 'Expect and Inline Tests' for details.` 66 ); 67 return []; 68 } 69 70 const workspaceItem = getWorkspaceItem(); 71 env.controller.items.add(workspaceItem); 72 const toDelete: vscode.TestItem[] = []; 73 // eslint-disable-next-line @typescript-eslint/no-extra-parens 74 toDelete.push(...(await addPPXTests(env, root, workspaceItem))); 75 76 return toDelete; 77 78 /** 79 * Return the `TestItem` of the current workspace if it does exist or create 80 * it. 81 * @returns The `TestItem` of the current workspace if it does exist or 82 * create it. 83 */ 84 function getWorkspaceItem() { 85 const item = env.controller.items.get(root.name); 86 if (item) { 87 return item; 88 } 89 90 return env.controller.createTestItem( 91 root.name, 92 c.workspaceLabel(root.name), 93 root.uri 94 ); 95 } 96} 97 98/** 99 * Run `opam env`, parse its output and set the environment accordingly. 100 * @param env The extension's environment. 101 * @param root The working directory for `opam`. 102 */ 103async function setOpamEnv(env: h.Env, root: vscode.WorkspaceFolder) { 104 const opamEnv = await io.opamEnv(root); 105 for (const oEnv of opamEnv) { 106 process.env[oEnv.name] = oEnv.value; 107 env.outChannel.appendLine( 108 `Workspace ${root.name}: adding env: ${oEnv.name} ${oEnv.value}` 109 ); 110 } 111} 112 113/** 114 * Add all inline and expect PPX test of the workspace `root`. 115 * @param env Everything needed to add these tests. 116 * @param root The workspace to add the tests to and from. 117 * @param workspaceItem The parent of the test tree in the Test Explorer view. 118 * @returns The list of `TestItems` that have been deleted from the Test 119 * Explorer tree. 120 */ 121async function addPPXTests( 122 env: h.Env, 123 root: vscode.WorkspaceFolder, 124 workspaceItem: vscode.TestItem 125) { 126 env.outChannel.appendLine( 127 `Workspace ${root.name}: searching for inline test runners ...` 128 ); 129 130 const inlineRunnerPaths = await io.findFilesRelative(root, c.runnerExeGlob); 131 const justBuildPaths = sanitizeRunnerPaths(env, inlineRunnerPaths); 132 if (!justBuildPaths.length) { 133 env.outChannel.appendLine( 134 `Workspace ${root.name}: no inline test runners found` 135 ); 136 } 137 for (const runner of justBuildPaths) { 138 env.outChannel.appendLine(`Found inline runner ${runner}`); 139 } 140 141 return generateTestList(env, { 142 runnerPaths: justBuildPaths, 143 root, 144 workspaceItem, 145 }); 146} 147 148/** 149 * Remove all test runners to exclude and test runner executables in `.sandbox`. 150 * @param env The extension's environment. 151 * @param inlineRunnerPaths The list of inline test runner paths to check. 152 * @returns The list of test runners without the ones to exclude. 153 */ 154function sanitizeRunnerPaths(env: h.Env, inlineRunnerPaths: string[]) { 155 const justBuildPaths: string[] = []; 156 const excludePaths = c.getCfgExcludeRunners(env.config); 157 158 // eslint-disable-next-line no-labels 159 outer: for (const incPath of inlineRunnerPaths) { 160 if (incPath.includes(c.sandboxDir)) { 161 // eslint-disable-next-line no-continue 162 continue; 163 } 164 for (const excPath of excludePaths) { 165 if (incPath.endsWith(excPath)) { 166 // eslint-disable-next-line no-labels, no-continue 167 continue outer; 168 } 169 } 170 171 justBuildPaths.push(incPath); 172 } 173 return justBuildPaths; 174} 175 176/** 177 * Generate the tree of tests in the Test Explorer from the list of tests of the 178 * test runners. 179 * @param env The environment to generate the tree of tests. 180 * @param data The data needed to generate the test tree. 181 * @returns The list of `TestItems` that have been deleted from the Test 182 * Explorer tree. 183 */ 184// eslint-disable-next-line max-statements 185async function generateTestList( 186 env: h.Env, 187 data: { 188 runnerPaths: string[]; 189 root: vscode.WorkspaceFolder; 190 workspaceItem: vscode.TestItem; 191 } 192) { 193 const groups: { name: string; tests: p.TestType[] }[] = []; 194 for (const rPath of data.runnerPaths) { 195 env.outChannel.appendLine(`Starting runner ${rPath}`); 196 // eslint-disable-next-line no-await-in-loop 197 const out = await io.runRunnerListDune({ 198 token: undefined, 199 root: data.root, 200 duneCmd: c.getCfgDunePath(env.config), 201 runner: rPath, 202 }); 203 env.outChannel.appendLine( 204 `Finished run: ${rPath}\nList of tests:\n${out.stdout}\nStderr: ${ 205 out.stderr 206 }\nError: ${out.error ? out.error : ""}` 207 ); 208 209 if (out.stdout) { 210 groups.push( 211 // eslint-disable-next-line @typescript-eslint/no-extra-parens, no-await-in-loop 212 ...(await parseTestListOutput(env, { 213 root: data.root, 214 workspaceItem: data.workspaceItem, 215 listOutput: out.stdout, 216 rPath, 217 })) 218 ); 219 } 220 } 221 const toDelete = deleteNonExistingGroups(data.workspaceItem, groups); 222 for (const del of toDelete) { 223 env.outChannel.appendLine(`Deleting ${del.label} ID: ${del.id}`); 224 } 225 return toDelete; 226} 227 228/** 229 * Parse the output of the test list and add the test items to the test tree. 230 * @param env The environment needed to add the tests. 231 * @param data The data needed to add the test item to the tree. 232 * @returns The list of tests parsed from the output. 233 */ 234// eslint-disable-next-line max-statements 235async function parseTestListOutput( 236 env: h.Env, 237 data: { 238 root: vscode.WorkspaceFolder; 239 workspaceItem: vscode.TestItem; 240 listOutput: string; 241 rPath: string; 242 } 243) { 244 const groups = p.parseTestList(data.listOutput); 245 246 for (const group of groups) { 247 // eslint-disable-next-line no-await-in-loop 248 const sourcePath = await io.findSourceOfTest(data.root, group.name); 249 const groupItem = getTestItem({ 250 controller: env.controller, 251 parent: data.workspaceItem, 252 id: group.name, 253 label: group.name, 254 uri: sourcePath, 255 }); 256 for (const t of group.tests) { 257 addTestItem(env, { 258 t, 259 sourcePath, 260 groupItem, 261 root: data.root, 262 runnerPath: data.rPath, 263 }); 264 } 265 } 266 return groups; 267} 268 269/** 270 * Check the given list of groups if there are any nodes in the tree, that have 271 * been deleted and delete these groups. 272 * That is, the group is not in the list of groups but is a children of the 273 * suite node. 274 * @param workspaceItem The workspace node the test belong to. 275 * @param groups The list of groups to check. 276 * @returns The list of `TestItems` that have been deleted from the Test 277 * Explorer tree. 278 */ 279function deleteNonExistingGroups( 280 workspaceItem: vscode.TestItem, 281 groups: { name: string; tests: p.TestType[] }[] 282) { 283 const workspaceGroup: vscode.TestItem[] = []; 284 workspaceItem.children.forEach((e) => workspaceGroup.push(e)); 285 const toDelete = workspaceGroup.filter( 286 (e) => !groups.find((v) => v.name === e.id) 287 ); 288 289 toDelete.forEach((e) => { 290 e.children.forEach((ch) => { 291 workspaceItem.children.delete(ch.id); 292 }); 293 workspaceItem.children.delete(e.id); 294 }); 295 296 return toDelete; 297} 298 299/** 300 * Add or update a single test item to the Test Explorer tree. 301 * @param env The environment needed to add the test. 302 * @param data The data needed to add the test item to the tree. 303 */ 304export function addTestItem( 305 env: h.Env, 306 data: { 307 t: p.TestType; 308 sourcePath: vscode.Uri; 309 groupItem: vscode.TestItem; 310 root: vscode.WorkspaceFolder; 311 runnerPath: string; 312 } 313) { 314 const testItem = getTestItem({ 315 controller: env.controller, 316 parent: data.groupItem, 317 label: data.t.name, 318 id: `${data.t.line}`, 319 uri: data.sourcePath, 320 line: data.t.line, 321 colStart: data.t.startCol, 322 colEnd: data.t.endCol, 323 }); 324 325 if (!env.testData.get(testItem)) { 326 env.testData.set(testItem, { 327 root: data.root, 328 runner: data.runnerPath, 329 library: p.getLibrary(data.runnerPath), 330 file: data.sourcePath.path, 331 }); 332 } 333} 334 335/** 336 * Return the `TestItem` with id `data.id`. 337 * If it does already exist, just update its name. If it does not yet exist, 338 * create a new `TestItem` and return that. 339 * @param data The needed data. 340 * @returns The existing or created `TestItem`. 341 */ 342function getTestItem(data: { 343 controller: vscode.TestController; 344 parent: vscode.TestItem; 345 id: string; 346 label: string; 347 line?: number; 348 colStart?: number; 349 colEnd?: number; 350 uri?: vscode.Uri; 351}) { 352 let item = data.parent.children.get(data.id); 353 if (item) { 354 item.label = data.label; 355 item.range = h.toRange( 356 data.line ? data.line - 1 : 0, 357 data.colStart, 358 data.colEnd 359 ); 360 return item; 361 } 362 363 item = data.controller.createTestItem(data.id, data.label, data.uri); 364 if (data.line) { 365 item.range = h.toRange( 366 data.line ? data.line - 1 : 0, 367 data.colStart, 368 data.colEnd 369 ); 370 } 371 372 data.parent.children.add(item); 373 374 return item; 375} 376 377/** 378 * Return a list of tests to run. 379 * 380 * Either all tests of the `controller` are run or only the ones specified in 381 * `request`. 382 * @param request The request which may hold a list of tests (`TestItem`s) to 383 * run. 384 * @param controller Holding all existing `TestItem`s. 385 * @param toDelete The list of deleted `TestItems`, don't add these to the tests 386 * to run. 387 * @returns The list of tests to run. 388 */ 389export function testList( 390 request: vscode.TestRunRequest, 391 controller: vscode.TestController, 392 toDelete: vscode.TestItem[] 393) { 394 const tests: vscode.TestItem[] = []; 395 396 if (request.include) { 397 request.include.forEach((t) => tests.push(...testAndChilds(t))); 398 } else { 399 controller.items.forEach((t) => tests.push(...testAndChilds(t))); 400 } 401 402 return tests.filter((t) => !request.exclude?.includes(t)); 403 404 /** 405 * Return a list of a test and its children, if it has any. 406 * @param t The test to check for children. 407 * @returns A list of a test and its children. 408 */ 409 function testAndChilds(t: vscode.TestItem) { 410 const testNChilds: vscode.TestItem[] = []; 411 if (t.children?.size > 0) { 412 t.children.forEach((el) => testNChilds.push(...testAndChilds(el))); 413 } else if (!toDelete.find((e) => e === t)) { 414 testNChilds.push(t); 415 } 416 417 return testNChilds; 418 } 419}