11import * as net from "node:net" ;
2+ import * as os from "node:os" ;
23
34import * as vscode from "vscode" ;
45
@@ -48,6 +49,11 @@ type MochaEvent =
4849 | [ "pending" , MochaTestEvent ]
4950 | [ "test-start" , MochaTestEvent ] ;
5051
52+ enum DebugMode {
53+ JS ,
54+ NATIVE_AND_JS ,
55+ }
56+
5157export function createTestController (
5258 context : vscode . ExtensionContext ,
5359 electronRoot : vscode . Uri ,
@@ -82,7 +88,7 @@ export function createTestController(
8288 async function runTests (
8389 request : vscode . TestRunRequest ,
8490 token : vscode . CancellationToken ,
85- debug : boolean = false ,
91+ debug ?: DebugMode ,
8692 ) {
8793 const extraArgs = request . profile
8894 ? runProfileData . get ( request . profile )
@@ -129,6 +135,18 @@ export function createTestController(
129135 }
130136 } ) ;
131137
138+ const processIdFilename = vscode . Uri . joinPath (
139+ context . storageUri ! ,
140+ ".native-test-debugging-process-id" ,
141+ ) ;
142+
143+ const env : Record < string , string > = {
144+ MOCHA_REPORTER : "mocha-multi-reporters" ,
145+ MOCHA_MULTI_REPORTERS : `${ context . asAbsolutePath (
146+ "out/electron/mocha-reporter.js" ,
147+ ) } , spec`,
148+ ELECTRON_ROOT : electronRoot . fsPath ,
149+ } ;
132150 let command = `${ buildToolsExecutable } test --runners=main` ;
133151
134152 if ( testRegexes . length ) {
@@ -139,10 +157,15 @@ export function createTestController(
139157 command += ` ${ extraArgs } ` ;
140158 }
141159
142- if ( debug ) {
160+ if ( debug !== undefined ) {
143161 command += ` --inspect-brk` ;
144162 }
145163
164+ if ( debug === DebugMode . NATIVE_AND_JS ) {
165+ command += ` --wait-for-debugger` ;
166+ env . ELECTRON_TEST_PID_DUMP_PATH = processIdFilename . fsPath ;
167+ }
168+
146169 // Mark all tests we're about to run as enqueued
147170 for ( const test of testsById . values ( ) ) {
148171 run . enqueued ( test ) ;
@@ -158,13 +181,7 @@ export function createTestController(
158181 cancellationToken : token ,
159182 shellOptions : {
160183 cwd : electronRoot . fsPath ,
161- env : {
162- MOCHA_REPORTER : "mocha-multi-reporters" ,
163- MOCHA_MULTI_REPORTERS : `${ context . asAbsolutePath (
164- "out/electron/mocha-reporter.js" ,
165- ) } , spec`,
166- ELECTRON_ROOT : electronRoot . fsPath ,
167- } ,
184+ env,
168185 } ,
169186 // Ignore non-zero exit codes, there's no way to
170187 // distinguish from normal test failures
@@ -238,7 +255,73 @@ export function createTestController(
238255 }
239256 } ) ;
240257
241- if ( debug ) {
258+ if ( debug === DebugMode . NATIVE_AND_JS ) {
259+ // Directory may not exist so create it first
260+ await vscode . workspace . fs . createDirectory ( context . storageUri ! ) ;
261+
262+ await vscode . workspace . fs . writeFile (
263+ processIdFilename ,
264+ new TextEncoder ( ) . encode ( "0" ) ,
265+ ) ;
266+
267+ // Watch for changes to the PID dump file so we know when
268+ // the Electron process is started and we can attach to it
269+ const watcher = vscode . workspace . createFileSystemWatcher (
270+ new vscode . RelativePattern (
271+ context . storageUri ! ,
272+ ".native-test-debugging-process-id" ,
273+ ) ,
274+ ) ;
275+ const processId = await new Promise < number > ( ( resolve , reject ) => {
276+ const timeoutId = setTimeout ( reject , 10_000 ) ;
277+
278+ watcher . onDidChange ( async ( uri ) => {
279+ clearTimeout ( timeoutId ) ;
280+ watcher . dispose ( ) ;
281+
282+ resolve (
283+ parseInt ( ( await vscode . workspace . fs . readFile ( uri ) ) . toString ( ) ) ,
284+ ) ;
285+ } ) ;
286+ } ) ;
287+
288+ const nativeDebuggingConfigurationType =
289+ os . platform ( ) === "win32"
290+ ? "electron.cpp.windows"
291+ : os . platform ( ) === "darwin"
292+ ? "electron.cpp.lldb"
293+ : "electron.cpp.gdb" ;
294+ const nativeDebuggingConfiguration =
295+ await context . extension . packageJSON . contributes . debuggers
296+ . find ( ( { type } ) => type === nativeDebuggingConfigurationType )
297+ ?. initialConfigurations . find ( ( { request } ) => request === "attach" ) ;
298+
299+ if ( ! nativeDebuggingConfiguration ) {
300+ testRunError = new vscode . TestMessage (
301+ "Couldn't find native debugging configuration" ,
302+ ) ;
303+ task . terminate ( ) ;
304+ return ;
305+ }
306+
307+ const nativeDebuggingSession = await vscode . debug . startDebugging (
308+ undefined ,
309+ {
310+ ...nativeDebuggingConfiguration ,
311+ processId,
312+ } ,
313+ { testRun : run } ,
314+ ) ;
315+
316+ if ( ! nativeDebuggingSession ) {
317+ testRunError = new vscode . TestMessage (
318+ "Couldn't start native debugging session" ,
319+ ) ;
320+ task . terminate ( ) ;
321+ }
322+ }
323+
324+ if ( testRunError === undefined && debug !== undefined ) {
242325 const debuggingSession = await vscode . debug . startDebugging (
243326 undefined ,
244327 {
@@ -248,7 +331,7 @@ export function createTestController(
248331 port : 9229 ,
249332 continueOnAttach : true ,
250333 } ,
251- { testRun : run } ,
334+ { testRun : run , parentSession : vscode . debug . activeDebugSession } ,
252335 ) ;
253336
254337 if ( ! debuggingSession ) {
@@ -281,33 +364,66 @@ export function createTestController(
281364 }
282365 }
283366
284- const runProfile = testController . createRunProfile (
285- "Run" ,
286- vscode . TestRunProfileKind . Run ,
287- async ( request , token ) => {
288- return ExtensionState . runOperation (
289- ExtensionOperation . RUN_TESTS ,
290- ( ) => runTests ( request , token ) ,
291- ExtensionState . isOperationRunning ( ExtensionOperation . RUN_TESTS ) ,
292- ) ;
293- } ,
294- true ,
295- ) ;
367+ const profiles = [
368+ testController . createRunProfile (
369+ "Run" ,
370+ vscode . TestRunProfileKind . Run ,
371+ async ( request , token ) => {
372+ return ExtensionState . runOperation (
373+ ExtensionOperation . RUN_TESTS ,
374+ ( ) => runTests ( request , token ) ,
375+ ExtensionState . isOperationRunning ( ExtensionOperation . RUN_TESTS ) ,
376+ ) ;
377+ } ,
378+ true ,
379+ ) ,
380+ testController . createRunProfile (
381+ "Debug" ,
382+ vscode . TestRunProfileKind . Debug ,
383+ async ( request , token ) => {
384+ return ExtensionState . runOperation (
385+ ExtensionOperation . RUN_TESTS ,
386+ ( ) => runTests ( request , token , DebugMode . JS ) ,
387+ ExtensionState . isOperationRunning ( ExtensionOperation . RUN_TESTS ) ,
388+ ) ;
389+ } ,
390+ false ,
391+ ) ,
392+ testController . createRunProfile (
393+ "Debug (C++ and JS)" ,
394+ vscode . TestRunProfileKind . Debug ,
395+ async ( request , token ) => {
396+ if ( ! vscode . extensions . getExtension ( "ms-vscode.cpptools" ) ) {
397+ vscode . window . showErrorMessage (
398+ "Please install the 'ms-vscode.cpptools' extension to enable native debugging" ,
399+ ) ;
400+ return ;
401+ }
296402
297- const debugProfile = testController . createRunProfile (
298- "Debug" ,
299- vscode . TestRunProfileKind . Debug ,
300- async ( request , token ) => {
301- return ExtensionState . runOperation (
302- ExtensionOperation . RUN_TESTS ,
303- ( ) => runTests ( request , token , true ) ,
304- ExtensionState . isOperationRunning ( ExtensionOperation . RUN_TESTS ) ,
305- ) ;
306- } ,
307- false ,
308- ) ;
403+ const specRunnerContents = await vscode . workspace . fs . readFile (
404+ vscode . Uri . joinPath ( electronRoot , "script" , "spec-runner.js" ) ,
405+ ) ;
406+
407+ if (
408+ ! specRunnerContents . toString ( ) . includes ( "ELECTRON_TEST_PID_DUMP_PATH" )
409+ ) {
410+ vscode . window . showErrorMessage (
411+ "This Electron checkout does not support native debugging - see https://github.com/electron/electron/pull/45481" ,
412+ ) ;
413+ return ;
414+ }
415+
416+ return ExtensionState . runOperation (
417+ ExtensionOperation . RUN_TESTS ,
418+ ( ) => runTests ( request , token , DebugMode . NATIVE_AND_JS ) ,
419+ ExtensionState . isOperationRunning ( ExtensionOperation . RUN_TESTS ) ,
420+ ) ;
421+ } ,
422+ false ,
423+ ) ,
424+ ] ;
309425
310- for ( const profile of [ runProfile , debugProfile ] ) {
426+ for ( const profile of profiles ) {
311427 profile . configureHandler = async ( ) => {
312428 const extraArgs = await vscode . window . showInputBox ( {
313429 title : "Electron Test Runner" ,
0 commit comments