55 *--------------------------------------------------------*/
66'use strict' ;
77
8+ import cp = require( 'child_process' ) ;
9+ import os = require( 'os' ) ;
810import path = require( 'path' ) ;
11+ import util = require( 'util' ) ;
912import vscode = require( 'vscode' ) ;
1013import { CommandFactory } from './commands' ;
1114import { getGoConfig } from './config' ;
1215import { GoExtensionContext } from './context' ;
16+ import { toolExecutionEnvironment } from './goEnv' ;
1317import { isModSupported } from './goModules' ;
1418import { escapeSubTestName } from './subTestUtils' ;
1519import {
@@ -25,6 +29,7 @@ import {
2529 SuiteToTestMap ,
2630 getTestFunctions
2731} from './testUtils' ;
32+ import { getBinPath } from './util' ;
2833
2934// lastTestConfig holds a reference to the last executed TestConfig which allows
3035// the last test to be easily re-executed.
@@ -346,6 +351,115 @@ export async function debugTestAtCursor(
346351 return await vscode . debug . startDebugging ( workspaceFolder , debugConfig ) ;
347352}
348353
354+ /**
355+ * Records and replays the test at cursor using Mozilla rr via `dlv replay`.
356+ * Only supported on Linux with rr installed.
357+ */
358+ export const rrAtCursor : CommandFactory = ( _ , goCtx ) => async ( args : any ) => {
359+ const editor = vscode . window . activeTextEditor ;
360+ if ( ! editor ) {
361+ vscode . window . showInformationMessage ( 'No editor is active.' ) ;
362+ return ;
363+ }
364+ if ( ! editor . document . fileName . endsWith ( '_test.go' ) ) {
365+ vscode . window . showInformationMessage ( 'No tests found. Current file is not a test file.' ) ;
366+ return ;
367+ }
368+ if ( os . platform ( ) !== 'linux' ) {
369+ vscode . window . showErrorMessage ( "'rr test' is only supported on Linux." ) ;
370+ return ;
371+ }
372+
373+ const rrPath = getBinPath ( 'rr' ) ;
374+ if ( ! rrPath || rrPath === 'rr' ) {
375+ // getBinPath returns the input unchanged when not found
376+ try {
377+ cp . execFileSync ( 'rr' , [ '--version' ] , { stdio : 'ignore' } ) ;
378+ } catch {
379+ vscode . window . showErrorMessage ( "'rr' not found on PATH. Install Mozilla rr to use this feature." ) ;
380+ return ;
381+ }
382+ }
383+
384+ const { testFunctions } = await getTestFunctionsAndTestSuite ( false , goCtx , editor . document ) ;
385+ const testFunctionName =
386+ args && args . functionName
387+ ? args . functionName
388+ : testFunctions ?. filter ( ( func ) => func . range . contains ( editor . selection . start ) ) . map ( ( el ) => el . name ) [ 0 ] ;
389+ if ( ! testFunctionName ) {
390+ vscode . window . showInformationMessage ( 'No test function found at cursor.' ) ;
391+ return ;
392+ }
393+
394+ await editor . document . save ( ) ;
395+
396+ const goConfig = getGoConfig ( editor . document . uri ) ;
397+ const pkgDir = path . dirname ( editor . document . fileName ) ;
398+ const traceDir = path . join ( os . tmpdir ( ) , `vscode-go-rr-${ Date . now ( ) } ` ) ;
399+ const testBin = path . join ( os . tmpdir ( ) , `vscode-go-rr-bin-${ Date . now ( ) } ` ) ;
400+
401+ const tags = getTestTags ( goConfig ) ;
402+ const buildFlags = tags ? [ '-tags' , tags ] : [ ] ;
403+ const flagsFromConfig = getTestFlags ( goConfig ) ;
404+ flagsFromConfig . forEach ( ( x ) => {
405+ if ( x !== '-args' ) buildFlags . push ( x ) ;
406+ } ) ;
407+
408+ const goRuntime = getBinPath ( 'go' ) ;
409+ const execFile = util . promisify ( cp . execFile ) ;
410+ const env = toolExecutionEnvironment ( ) ;
411+
412+ try {
413+ await execFile ( goRuntime , [ 'test' , '-c' , '-o' , testBin , ...buildFlags , pkgDir ] , { env, cwd : pkgDir } ) ;
414+ } catch ( e : any ) {
415+ vscode . window . showErrorMessage ( `Failed to build test binary: ${ e . message ?? e } ` ) ;
416+ return ;
417+ }
418+
419+ const workspaceFolder = vscode . workspace . getWorkspaceFolder ( editor . document . uri ) ;
420+
421+ const exitCode = await new Promise < number > ( ( resolve ) => {
422+ const writeEmitter = new vscode . EventEmitter < string > ( ) ;
423+ const pty : vscode . Pseudoterminal = {
424+ onDidWrite : writeEmitter . event ,
425+ open ( ) {
426+ const proc = cp . spawn (
427+ 'rr' ,
428+ [ 'record' , `--output-trace-dir=${ traceDir } ` , testBin , '-test.run' , `^${ testFunctionName } $` ] ,
429+ { cwd : pkgDir , env : { ...process . env , ...env } }
430+ ) ;
431+ const onData = ( chunk : Buffer | string ) => {
432+ writeEmitter . fire ( chunk . toString ( ) . replace ( / \n / g, '\r\n' ) ) ;
433+ } ;
434+ proc . stdout ?. on ( 'data' , onData ) ;
435+ proc . stderr ?. on ( 'data' , onData ) ;
436+ proc . on ( 'close' , ( code ) => {
437+ writeEmitter . fire ( `\r\nrr exited with code ${ code } .\r\n` ) ;
438+ resolve ( code ?? 1 ) ;
439+ } ) ;
440+ } ,
441+ close ( ) { }
442+ } ;
443+ vscode . window . createTerminal ( { name : `rr: ${ testFunctionName } ` , pty } ) . show ( ) ;
444+ } ) ;
445+
446+ if ( exitCode !== 0 ) {
447+ vscode . window . showErrorMessage ( `rr record failed (exit code ${ exitCode } ). Check the terminal for details.` ) ;
448+ return ;
449+ }
450+
451+ const debugConfig : vscode . DebugConfiguration = {
452+ name : `rr replay: ${ testFunctionName } ` ,
453+ type : 'go' ,
454+ request : 'launch' ,
455+ mode : 'replay' ,
456+ traceDirPath : traceDir ,
457+ env : goConfig . get ( 'testEnvVars' , { } ) ,
458+ envFile : goConfig . get ( 'testEnvFile' )
459+ } ;
460+ await vscode . debug . startDebugging ( workspaceFolder , debugConfig ) ;
461+ } ;
462+
349463/**
350464 * Runs all tests in the package of the source of the active editor.
351465 *
0 commit comments