|
1 | | -import { inject, injectable } from 'inversify'; |
| 1 | +import { inject, injectable, named } from 'inversify'; |
2 | 2 | import * as path from 'path'; |
3 | | -import { Uri } from 'vscode'; |
4 | | -import { IDebugService, IWorkspaceService } from '../../common/application/types'; |
| 3 | +import * as stripJsonComments from 'strip-json-comments'; |
| 4 | +import { DebugConfiguration, Uri, WorkspaceFolder } from 'vscode'; |
| 5 | +import { IApplicationShell, IDebugService, IWorkspaceService } from '../../common/application/types'; |
5 | 6 | import { EXTENSION_ROOT_DIR } from '../../common/constants'; |
6 | | -import { IConfigurationService } from '../../common/types'; |
7 | | -import { DebugOptions } from '../../debugger/types'; |
| 7 | +import { traceError } from '../../common/logger'; |
| 8 | +import { IFileSystem } from '../../common/platform/types'; |
| 9 | +import { IConfigurationService, IPythonSettings } from '../../common/types'; |
| 10 | +import { noop } from '../../common/utils/misc'; |
| 11 | +import { DebuggerTypeName } from '../../debugger/constants'; |
| 12 | +import { IDebugConfigurationResolver } from '../../debugger/extension/configuration/types'; |
| 13 | +import { LaunchRequestArguments } from '../../debugger/types'; |
8 | 14 | import { IServiceContainer } from '../../ioc/types'; |
9 | | -import { ITestDebugLauncher, LaunchOptions, TestProvider } from './types'; |
| 15 | +import { |
| 16 | + ITestDebugConfig, ITestDebugLauncher, LaunchOptions, TestProvider |
| 17 | +} from './types'; |
10 | 18 |
|
11 | 19 | @injectable() |
12 | 20 | export class DebugLauncher implements ITestDebugLauncher { |
13 | | - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { } |
| 21 | + private readonly configService: IConfigurationService; |
| 22 | + private readonly workspaceService: IWorkspaceService; |
| 23 | + private readonly fs: IFileSystem; |
| 24 | + constructor( |
| 25 | + @inject(IServiceContainer) private serviceContainer: IServiceContainer, |
| 26 | + @inject(IDebugConfigurationResolver) @named('launch') private readonly launchResolver: IDebugConfigurationResolver<LaunchRequestArguments> |
| 27 | + ) { |
| 28 | + this.configService = this.serviceContainer.get<IConfigurationService>(IConfigurationService); |
| 29 | + this.workspaceService = this.serviceContainer.get<IWorkspaceService>(IWorkspaceService); |
| 30 | + this.fs = this.serviceContainer.get<IFileSystem>(IFileSystem); |
| 31 | + } |
| 32 | + |
14 | 33 | public async launchDebugger(options: LaunchOptions) { |
15 | 34 | if (options.token && options.token!.isCancellationRequested) { |
16 | 35 | return; |
17 | 36 | } |
18 | | - const cwdUri = options.cwd ? Uri.file(options.cwd) : undefined; |
19 | | - const workspaceService = this.serviceContainer.get<IWorkspaceService>(IWorkspaceService); |
20 | | - if (!workspaceService.hasWorkspaceFolders) { |
| 37 | + |
| 38 | + const workspaceFolder = this.resolveWorkspaceFolder(options.cwd); |
| 39 | + const launchArgs = await this.getLaunchArgs( |
| 40 | + options, |
| 41 | + workspaceFolder, |
| 42 | + this.configService.getSettings(workspaceFolder.uri) |
| 43 | + ); |
| 44 | + const debugManager = this.serviceContainer.get<IDebugService>(IDebugService); |
| 45 | + return debugManager.startDebugging(workspaceFolder, launchArgs) |
| 46 | + .then(noop, ex => traceError('Failed to start debugging tests', ex)); |
| 47 | + } |
| 48 | + |
| 49 | + private resolveWorkspaceFolder(cwd: string): WorkspaceFolder { |
| 50 | + if (!this.workspaceService.hasWorkspaceFolders) { |
21 | 51 | throw new Error('Please open a workspace'); |
22 | 52 | } |
23 | | - let workspaceFolder = workspaceService.getWorkspaceFolder(cwdUri!); |
| 53 | + |
| 54 | + const cwdUri = cwd ? Uri.file(cwd) : undefined; |
| 55 | + let workspaceFolder = this.workspaceService.getWorkspaceFolder(cwdUri!); |
24 | 56 | if (!workspaceFolder) { |
25 | | - workspaceFolder = workspaceService.workspaceFolders![0]; |
| 57 | + workspaceFolder = this.workspaceService.workspaceFolders![0]; |
26 | 58 | } |
| 59 | + return workspaceFolder; |
| 60 | + } |
27 | 61 |
|
28 | | - const cwd = cwdUri ? cwdUri.fsPath : workspaceFolder.uri.fsPath; |
29 | | - const configSettings = this.serviceContainer.get<IConfigurationService>(IConfigurationService).getSettings(Uri.file(cwd)); |
30 | | - const debugManager = this.serviceContainer.get<IDebugService>(IDebugService); |
31 | | - const debugArgs = this.fixArgs(options.args, options.testProvider); |
32 | | - const program = this.getTestLauncherScript(options.testProvider); |
33 | | - return debugManager.startDebugging(workspaceFolder, { |
34 | | - name: 'Debug Unit Test', |
35 | | - type: 'python', |
36 | | - request: 'launch', |
37 | | - program, |
38 | | - cwd, |
39 | | - args: debugArgs, |
40 | | - console: 'none', |
41 | | - envFile: configSettings.envFile, |
42 | | - debugOptions: [DebugOptions.RedirectOutput] |
43 | | - }).then(() => void (0)); |
| 62 | + private async getLaunchArgs( |
| 63 | + options: LaunchOptions, |
| 64 | + workspaceFolder: WorkspaceFolder, |
| 65 | + configSettings: IPythonSettings |
| 66 | + ): Promise<LaunchRequestArguments> { |
| 67 | + let debugConfig = await this.readDebugConfig(); |
| 68 | + if (!debugConfig) { |
| 69 | + debugConfig = { |
| 70 | + name: 'Debug Unit Test', |
| 71 | + type: 'python', |
| 72 | + request: 'test' |
| 73 | + }; |
| 74 | + } |
| 75 | + this.applyDefaults(debugConfig!, workspaceFolder, configSettings); |
| 76 | + |
| 77 | + return this.convertConfigToArgs(debugConfig!, workspaceFolder, options); |
| 78 | + } |
| 79 | + |
| 80 | + private async readDebugConfig(): Promise<ITestDebugConfig | undefined> { |
| 81 | + const configs = await this.readAllDebugConfigs(); |
| 82 | + for (const cfg of configs) { |
| 83 | + if (!cfg.name || cfg.type !== DebuggerTypeName || cfg.request !== 'test') { |
| 84 | + continue; |
| 85 | + } |
| 86 | + // Return the first one. |
| 87 | + return cfg as ITestDebugConfig; |
| 88 | + } |
| 89 | + return undefined; |
| 90 | + } |
| 91 | + |
| 92 | + private async readAllDebugConfigs(): Promise<DebugConfiguration[]> { |
| 93 | + const workspaceFolder = this.workspaceService.workspaceFolders![0]; |
| 94 | + const filename = path.join(workspaceFolder.uri.fsPath, '.vscode', 'launch.json'); |
| 95 | + let configs: DebugConfiguration[] = []; |
| 96 | + try { |
| 97 | + let text = await this.fs.readFile(filename); |
| 98 | + text = stripJsonComments(text); |
| 99 | + const parsed = JSON.parse(text); |
| 100 | + if (!parsed.version || !parsed.configurations || !Array.isArray(parsed.configurations)) { |
| 101 | + throw Error('malformed launch.json'); |
| 102 | + } |
| 103 | + // We do not bother ensuring each item is a DebugConfiguration... |
| 104 | + configs = parsed.configurations; |
| 105 | + } catch (exc) { |
| 106 | + traceError('could not get debug config', exc); |
| 107 | + const appShell = this.serviceContainer.get<IApplicationShell>(IApplicationShell); |
| 108 | + await appShell.showErrorMessage('could not load unit test config from launch.json'); |
| 109 | + } |
| 110 | + return configs; |
44 | 111 | } |
| 112 | + |
| 113 | + private applyDefaults( |
| 114 | + cfg: ITestDebugConfig, |
| 115 | + workspaceFolder: WorkspaceFolder, |
| 116 | + configSettings: IPythonSettings |
| 117 | + ) { |
| 118 | + // cfg.pythonPath is handled by LaunchConfigurationResolver. |
| 119 | + if (!cfg.console) { |
| 120 | + cfg.console = 'none'; |
| 121 | + } |
| 122 | + if (!cfg.cwd) { |
| 123 | + cfg.cwd = workspaceFolder.uri.fsPath; |
| 124 | + } |
| 125 | + if (!cfg.env) { |
| 126 | + cfg.env = {}; |
| 127 | + } |
| 128 | + if (!cfg.envFile) { |
| 129 | + cfg.envFile = configSettings.envFile; |
| 130 | + } |
| 131 | + |
| 132 | + if (cfg.stopOnEntry === undefined) { |
| 133 | + cfg.stopOnEntry = false; |
| 134 | + } |
| 135 | + if (cfg.showReturnValue === undefined) { |
| 136 | + cfg.showReturnValue = false; |
| 137 | + } |
| 138 | + if (cfg.redirectOutput === undefined) { |
| 139 | + cfg.redirectOutput = true; |
| 140 | + } |
| 141 | + if (cfg.debugStdLib === undefined) { |
| 142 | + cfg.debugStdLib = false; |
| 143 | + } |
| 144 | + } |
| 145 | + |
| 146 | + private async convertConfigToArgs( |
| 147 | + debugConfig: ITestDebugConfig, |
| 148 | + workspaceFolder: WorkspaceFolder, |
| 149 | + options: LaunchOptions |
| 150 | + ): Promise<LaunchRequestArguments> { |
| 151 | + const configArgs = debugConfig as LaunchRequestArguments; |
| 152 | + |
| 153 | + configArgs.program = this.getTestLauncherScript(options.testProvider); |
| 154 | + configArgs.args = this.fixArgs(options.args, options.testProvider); |
| 155 | + // We leave configArgs.request as "test" so it will be sent in telemetry. |
| 156 | + |
| 157 | + const launchArgs = await this.launchResolver.resolveDebugConfiguration( |
| 158 | + workspaceFolder, |
| 159 | + configArgs, |
| 160 | + options.token |
| 161 | + ); |
| 162 | + if (!launchArgs) { |
| 163 | + throw Error(`Invalid debug config "${debugConfig.name}"`); |
| 164 | + } |
| 165 | + launchArgs.request = 'launch'; |
| 166 | + |
| 167 | + return launchArgs!; |
| 168 | + } |
| 169 | + |
45 | 170 | private fixArgs(args: string[], testProvider: TestProvider): string[] { |
46 | 171 | if (testProvider === 'unittest') { |
47 | 172 | return args.filter(item => item !== '--debug'); |
48 | 173 | } else { |
49 | 174 | return args; |
50 | 175 | } |
51 | 176 | } |
| 177 | + |
52 | 178 | private getTestLauncherScript(testProvider: TestProvider) { |
53 | 179 | switch (testProvider) { |
54 | 180 | case 'unittest': { |
|
0 commit comments