|
| 1 | +import * as vscode from 'vscode'; |
| 2 | +import { allTestRuns, extensionId, IServerSpec, osAPI } from './extension'; |
| 3 | +import { relativeTestRoot } from './localTests'; |
| 4 | +import logger from './logger'; |
| 5 | + |
| 6 | +export async function commonRunTestsHandler(controller: vscode.TestController, resolveItemChildren: (item: vscode.TestItem) => Promise<void>, request: vscode.TestRunRequest, cancellation: vscode.CancellationToken) { |
| 7 | + logger.info(`commonRunTestsHandler invoked by controller id=${controller.id}`); |
| 8 | + |
| 9 | + const isResolvedMap = new WeakMap<vscode.TestItem, boolean>(); |
| 10 | + |
| 11 | + // For each authority (i.e. server:namespace) accumulate a map of the class-level Test nodes in the tree. |
| 12 | + // We don't yet support running only some TestXXX methods in a testclass |
| 13 | + const mapAuthorities = new Map<string, Map<string, vscode.TestItem>>(); |
| 14 | + const queue: vscode.TestItem[] = []; |
| 15 | + |
| 16 | + // Loop through all included tests, or all known tests, and add them to our queue |
| 17 | + if (request.include) { |
| 18 | + request.include.forEach(test => queue.push(test)); |
| 19 | + } else { |
| 20 | + controller.items.forEach(test => queue.push(test)); |
| 21 | + } |
| 22 | + |
| 23 | + // Process every test that was queued. Recurse down to leaves (testmethods) and build a map of their parents (classes) |
| 24 | + while (queue.length > 0 && !cancellation.isCancellationRequested) { |
| 25 | + const test = queue.pop()!; |
| 26 | + |
| 27 | + // Skip tests the user asked to exclude |
| 28 | + if (request.exclude?.includes(test)) { |
| 29 | + continue; |
| 30 | + } |
| 31 | + |
| 32 | + // Resolve children if not already done |
| 33 | + if (test.canResolveChildren && !isResolvedMap.get(test)) { |
| 34 | + await resolveItemChildren(test); |
| 35 | + } |
| 36 | + |
| 37 | + // If a leaf item (a TestXXX method in a class) note its .cls file for copying. |
| 38 | + // Every leaf must have a uri. |
| 39 | + if (test.children.size === 0 && test.uri && test.parent) { |
| 40 | + let authority = test.uri.authority; |
| 41 | + let key = test.uri.path; |
| 42 | + if (test.uri.scheme === "file") { |
| 43 | + // Client-side editing, for which we will assume objectscript.conn names a server defined in `intersystems.servers` |
| 44 | + const conn: any = vscode.workspace.getConfiguration("objectscript", test.uri).get("conn"); |
| 45 | + authority = conn.server + ":" + (conn.ns as string).toLowerCase(); |
| 46 | + const folder = vscode.workspace.getWorkspaceFolder(test.uri); |
| 47 | + if (folder) { |
| 48 | + key = key.slice(folder.uri.path.length + relativeTestRoot(folder).length + 1); |
| 49 | + } |
| 50 | + } |
| 51 | + |
| 52 | + const mapTestClasses = mapAuthorities.get(authority) || new Map<string, vscode.TestItem>(); |
| 53 | + mapTestClasses.set(key, test.parent); |
| 54 | + mapAuthorities.set(authority, mapTestClasses); |
| 55 | + } |
| 56 | + |
| 57 | + // Queue any children |
| 58 | + test.children.forEach(test => queue.push(test)); |
| 59 | + } |
| 60 | + |
| 61 | + if (mapAuthorities.size === 0) { |
| 62 | + // Nothing included |
| 63 | + vscode.window.showWarningMessage(`Empty test run`); |
| 64 | + return; |
| 65 | + } |
| 66 | + |
| 67 | + if (cancellation.isCancellationRequested) { |
| 68 | + // TODO what? |
| 69 | + } |
| 70 | + |
| 71 | + for await (const mapInstance of mapAuthorities) { |
| 72 | + |
| 73 | + const run = controller.createTestRun( |
| 74 | + request, |
| 75 | + 'Test Results', |
| 76 | + true |
| 77 | + ); |
| 78 | + const authority = mapInstance[0]; |
| 79 | + const mapTestClasses = mapInstance[1]; |
| 80 | + const firstClassTestItem = Array.from(mapTestClasses.values())[0]; |
| 81 | + const oneUri = firstClassTestItem.uri; |
| 82 | + |
| 83 | + // This will always be true since every test added to the map above required a uri |
| 84 | + if (oneUri) { |
| 85 | + |
| 86 | + // First, clear out the server-side folder for the classes whose testmethods will be run |
| 87 | + const folder = vscode.workspace.getWorkspaceFolder(oneUri); |
| 88 | + const server = osAPI.serverForUri(oneUri); |
| 89 | + const username = server.username || 'UnknownUser'; |
| 90 | + const testRoot = vscode.Uri.from({ scheme: 'isfs', authority, path: `/.vscode/UnitTestRoot/${username}` }); |
| 91 | + try { |
| 92 | + // Limitation of the Atelier API means this can only delete the files, not the folders |
| 93 | + // but zombie folders shouldn't cause problems. |
| 94 | + await vscode.workspace.fs.delete(testRoot, { recursive: true }); |
| 95 | + } catch (error) { |
| 96 | + console.log(error); |
| 97 | + } |
| 98 | + |
| 99 | + // Next, copy the classes into the folder as a package hierarchy |
| 100 | + for await (const mapInstance of mapTestClasses) { |
| 101 | + const key = mapInstance[0]; |
| 102 | + const classTest = mapInstance[1]; |
| 103 | + const uri = classTest.uri; |
| 104 | + const keyParts = key.split('/'); |
| 105 | + const clsFile = keyParts.pop() || ''; |
| 106 | + const directoryUri = testRoot.with({ path: testRoot.path.concat(keyParts.join('/') + '/') }); |
| 107 | + // This will always be true since every test added to the map above required a uri |
| 108 | + if (uri) { |
| 109 | + try { |
| 110 | + await vscode.workspace.fs.copy(uri, directoryUri.with({ path: directoryUri.path.concat(clsFile) })); |
| 111 | + } catch (error) { |
| 112 | + console.log(error); |
| 113 | + continue; |
| 114 | + } |
| 115 | + |
| 116 | + // Unless the file copy failed, enqueue all the testitems that represent the TestXXX methods of the class |
| 117 | + classTest.children.forEach((methodTest) => { |
| 118 | + run.enqueued(methodTest); |
| 119 | + }); |
| 120 | + } |
| 121 | + } |
| 122 | + |
| 123 | + // Finally, run the tests using the debugger API |
| 124 | + const serverSpec: IServerSpec = { |
| 125 | + username: server.username, |
| 126 | + name: server.serverName, |
| 127 | + webServer: { |
| 128 | + host: server.host, |
| 129 | + port: server.port, |
| 130 | + pathPrefix: server.pathPrefix, |
| 131 | + scheme: server.scheme |
| 132 | + } |
| 133 | + }; |
| 134 | + const namespace: string = server.namespace.toUpperCase(); |
| 135 | + const runQualifiers = controller.id === `${extensionId}-Local` ? "" : "/noload/nodelete"; |
| 136 | + // Run tests through the debugger but only stop at breakpoints etc if user chose "Debug Test" instead of "Run Test" |
| 137 | + const runIndex = allTestRuns.push(run) - 1; |
| 138 | + const configuration: vscode.DebugConfiguration = { |
| 139 | + "type": "objectscript", |
| 140 | + "request": "launch", |
| 141 | + "name": `${controller.id.split("-").pop()}Tests:${serverSpec.name}:${namespace}:${serverSpec.username}`, |
| 142 | + "program": `##class(%UnitTest.Manager).RunTest("${serverSpec.username}","${runQualifiers}")`, |
| 143 | + "testRunIndex": runIndex, |
| 144 | + "testIdBase": firstClassTestItem.id.split(":", 2).join(":") |
| 145 | + }; |
| 146 | + const sessionOptions: vscode.DebugSessionOptions = { |
| 147 | + noDebug: request.profile?.kind !== vscode.TestRunProfileKind.Debug, |
| 148 | + suppressDebugToolbar: request.profile?.kind !== vscode.TestRunProfileKind.Debug |
| 149 | + }; |
| 150 | + if (!await vscode.debug.startDebugging(folder, configuration, sessionOptions)) { |
| 151 | + await vscode.window.showErrorMessage(`Failed to launch testing`, { modal: true }); |
| 152 | + run.end(); |
| 153 | + allTestRuns[runIndex] = undefined; |
| 154 | + return; |
| 155 | + } |
| 156 | + } |
| 157 | + } |
| 158 | +} |
0 commit comments