diff --git a/packages/extension/src/TestExecution/ProcessBuilderFactory.ts b/packages/extension/src/TestExecution/ProcessBuilderFactory.ts index ff9f95a6..60ebade8 100644 --- a/packages/extension/src/TestExecution/ProcessBuilderFactory.ts +++ b/packages/extension/src/TestExecution/ProcessBuilderFactory.ts @@ -7,19 +7,25 @@ import { Xdebug, } from '@vscode-phpunit/phpunit'; import { inject, injectable } from 'inversify'; -import { TestRunProfileKind } from 'vscode'; +import { TestRunProfileKind, type WorkspaceFolder } from 'vscode'; import { Configuration } from '../Configuration'; +import { TYPES } from '../types'; @injectable() export class ProcessBuilderFactory { constructor( @inject(Configuration) private config: Configuration, @inject(PHPUnitXML) private phpUnitXML: PHPUnitXML, + @inject(TYPES.WorkspaceFolder) private workspaceFolder: WorkspaceFolder, ) {} async create(profileKind?: TestRunProfileKind): Promise { const options = { cwd: this.phpUnitXML.root() }; - const pathReplacer = new PathReplacer(options, this.config.get('paths') as Path); + const pathReplacer = new PathReplacer( + options, + this.config.get('paths') as Path, + this.workspaceFolder.uri.fsPath, + ); const xdebug = await new Xdebug(this.config).setMode(this.toMode(profileKind)); return new ProcessBuilder(this.config, options, pathReplacer, xdebug); } diff --git a/packages/phpunit/src/Configuration/PathReplacer.test.ts b/packages/phpunit/src/Configuration/PathReplacer.test.ts index bdb577e8..cbefd406 100644 --- a/packages/phpunit/src/Configuration/PathReplacer.test.ts +++ b/packages/phpunit/src/Configuration/PathReplacer.test.ts @@ -12,23 +12,33 @@ import { import { PathReplacer } from './PathReplacer'; describe('PathReplacer', () => { - const givenPathReplacer = (paths?: Record, cwd?: string) => { + const givenPathReplacer = ( + paths?: Record, + cwd?: string, + workspaceFolder?: string, + ) => { return new PathReplacer( { cwd: cwd ?? phpUnitProject('') }, paths ?? { [VAR_WORKSPACE_FOLDER]: '/app', }, + workspaceFolder, ); }; const toWindows = (path: string) => path.replace(/\//g, '\\').replace(/\\$/g, ''); - const givenPathReplacerForWindows = (paths?: Record, cwd?: string) => { + const givenPathReplacerForWindows = ( + paths?: Record, + cwd?: string, + workspaceFolder?: string, + ) => { return new PathReplacer( { cwd: cwd ?? phpUnitProjectWin('') }, paths ?? { [VAR_WORKSPACE_FOLDER]: '/app', }, + workspaceFolder, ); }; @@ -282,4 +292,52 @@ describe('PathReplacer', () => { const remotePath = '/app/tests/AssertionsTest.php'; expect(pathReplacer.toLocal(remotePath)).toEqual(remotePath); }); + + describe('workspaceFolder differs from cwd (monorepo / nested config)', () => { + const workspace = '/workspace'; + const cwd = '/workspace/apps/api'; + + it(`replaces ${VAR_WORKSPACE_FOLDER} with workspaceFolder, not cwd`, () => { + const pathReplacer = new PathReplacer({ cwd }, undefined, workspace); + + expect( + pathReplacer.toRemote(`${VAR_WORKSPACE_FOLDER}/apps/api/phpunit.xml.dist`), + ).toEqual('/workspace/apps/api/phpunit.xml.dist'); + }); + + it(`replaces ${VAR_WORKSPACE_FOLDER_BASENAME} with basename of workspaceFolder, not cwd`, () => { + const pathReplacer = new PathReplacer({ cwd }, undefined, workspace); + + expect(pathReplacer.replacePathVariables(`${VAR_WORKSPACE_FOLDER_BASENAME}`)).toEqual( + 'workspace', + ); + }); + + it(`replaces ${VAR_PWD} with cwd, not workspaceFolder`, () => { + const pathReplacer = new PathReplacer({ cwd }, undefined, workspace); + + expect(pathReplacer.toRemote(`${VAR_PWD}/vendor/bin/phpunit`)).toEqual( + '/workspace/apps/api/vendor/bin/phpunit', + ); + }); + + it(`does not double-nest path when ${VAR_WORKSPACE_FOLDER} contains subdirectory`, () => { + const pathReplacer = new PathReplacer({ cwd }, undefined, workspace); + + const result = pathReplacer.toRemote( + `${VAR_WORKSPACE_FOLDER}/apps/api/vendor/bin/phpunit`, + ); + + expect(result).toEqual('/workspace/apps/api/vendor/bin/phpunit'); + expect(result).not.toContain('apps/api/apps/api'); + }); + + it(`falls back to cwd when workspaceFolder is not provided`, () => { + const pathReplacer = new PathReplacer({ cwd }); + + expect( + pathReplacer.toRemote(`${VAR_WORKSPACE_FOLDER}/apps/api/phpunit.xml.dist`), + ).toEqual('/workspace/apps/api/apps/api/phpunit.xml.dist'); + }); + }); }); diff --git a/packages/phpunit/src/Configuration/PathReplacer.ts b/packages/phpunit/src/Configuration/PathReplacer.ts index 1370a469..a4556b57 100644 --- a/packages/phpunit/src/Configuration/PathReplacer.ts +++ b/packages/phpunit/src/Configuration/PathReplacer.ts @@ -29,16 +29,20 @@ export class PathReplacer { constructor( private options: SpawnOptions = {}, paths?: Path, + workspaceFolder?: string, ) { this.cwd = this.fixWindowsDriveLetter( (this.options?.cwd as string) ?? (process.env.cwd as string), ); + const workspaceFolderPath = workspaceFolder + ? this.fixWindowsDriveLetter(workspaceFolder) + : this.cwd; this.pathVariables = new Map(); this.pathVariables.set(VAR_PWD, this.cwd); - this.pathVariables.set(VAR_WORKSPACE_FOLDER, this.cwd); + this.pathVariables.set(VAR_WORKSPACE_FOLDER, workspaceFolderPath); this.pathVariables.set( VAR_WORKSPACE_FOLDER_BASENAME, - this.cwd ? path.basename(this.cwd) : '', + workspaceFolderPath ? path.basename(workspaceFolderPath) : '', ); this.pathVariables.set(VAR_USER_HOME, os.homedir()); this.pathVariables.set(VAR_PATH_SEPARATOR, path.sep);