Skip to content

Commit c0172dd

Browse files
test: add run-button uv-mode test cases
Agent-Logs-Url: https://github.com/microsoft/vscode-python-environments/sessions/ee61d4e1-336b-405a-93dc-f691bd210c7e Co-authored-by: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com>
1 parent d8ecfda commit c0172dd

1 file changed

Lines changed: 279 additions & 0 deletions

File tree

src/test/features/execution/runAsTask.unit.test.ts

Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,285 @@ suite('runAsTask Tests', () => {
476476
});
477477
});
478478

479+
suite('UV Mode Scenarios', () => {
480+
test('should pass project URI as scope to shouldUseUv', async () => {
481+
// Mock - Verify per-folder setting precedence by passing project.uri as the scope
482+
const projectUri = Uri.file('/workspace/project');
483+
const environment: PythonEnvironment = {
484+
envId: { id: 'test-env', managerId: 'test-manager' },
485+
name: 'Test Environment',
486+
displayName: 'Test Environment',
487+
displayPath: '/path/to/env',
488+
version: '3.9.0',
489+
environmentPath: Uri.file('/path/to/env'),
490+
execInfo: {
491+
run: { executable: '/path/to/python', args: [] },
492+
},
493+
sysPrefix: '/path/to/env',
494+
};
495+
496+
const options: PythonTaskExecutionOptions = {
497+
name: 'Scoped Task',
498+
args: ['script.py'],
499+
project: { name: 'Test Project', uri: projectUri },
500+
};
501+
502+
mockGetWorkspaceFolder.withArgs(projectUri).returns(undefined);
503+
mockShouldUseUv.resolves(false);
504+
mockQuoteStringIfNecessary.withArgs('/path/to/python').returns('/path/to/python');
505+
mockExecuteTask.resolves({} as TaskExecution);
506+
507+
// Run
508+
await runAsTask(environment, options);
509+
510+
// Assert - shouldUseUv was called with the project URI as the third (scope) argument
511+
assert.ok(
512+
mockShouldUseUv.calledWith(undefined, environment.environmentPath.fsPath, projectUri),
513+
'Should pass project URI as the scope argument to shouldUseUv',
514+
);
515+
});
516+
517+
test('should pass undefined scope to shouldUseUv when project is not provided', async () => {
518+
// Mock - No project means no scope, so shouldUseUv resolves the user/global setting
519+
const environment: PythonEnvironment = {
520+
envId: { id: 'test-env', managerId: 'test-manager' },
521+
name: 'Test Environment',
522+
displayName: 'Test Environment',
523+
displayPath: '/path/to/env',
524+
version: '3.9.0',
525+
environmentPath: Uri.file('/path/to/env'),
526+
execInfo: {
527+
run: { executable: '/path/to/python', args: [] },
528+
},
529+
sysPrefix: '/path/to/env',
530+
};
531+
532+
const options: PythonTaskExecutionOptions = {
533+
name: 'No-Scope Task',
534+
args: ['script.py'],
535+
};
536+
537+
mockGetWorkspaceFolder.returns(undefined);
538+
mockShouldUseUv.resolves(false);
539+
mockQuoteStringIfNecessary.withArgs('/path/to/python').returns('/path/to/python');
540+
mockExecuteTask.resolves({} as TaskExecution);
541+
542+
// Run
543+
await runAsTask(environment, options);
544+
545+
// Assert - third argument is explicitly undefined when project is missing
546+
assert.ok(
547+
mockShouldUseUv.calledWith(undefined, environment.environmentPath.fsPath, undefined),
548+
'Should pass undefined scope when no project is provided',
549+
);
550+
});
551+
552+
test('should fall back to run.executable in --python when activatedRun is missing under uv', async () => {
553+
// Mock - Env has only run, no activatedRun; uv mode is on
554+
const environment: PythonEnvironment = {
555+
envId: { id: 'test-env', managerId: 'test-manager' },
556+
name: 'Test Environment',
557+
displayName: 'Test Environment',
558+
displayPath: '/path/to/env',
559+
version: '3.9.0',
560+
environmentPath: Uri.file('/path/to/env'),
561+
execInfo: {
562+
run: { executable: '/path/to/python', args: ['-X', 'utf8'] },
563+
},
564+
sysPrefix: '/path/to/env',
565+
};
566+
567+
const options: PythonTaskExecutionOptions = {
568+
name: 'Fallback Run UV Task',
569+
args: ['script.py'],
570+
};
571+
572+
mockGetWorkspaceFolder.returns(undefined);
573+
mockShouldUseUv.resolves(true);
574+
mockQuoteStringIfNecessary.withArgs('uv').returns('uv');
575+
mockExecuteTask.resolves({} as TaskExecution);
576+
577+
// Run
578+
await runAsTask(environment, options);
579+
580+
// Assert
581+
const taskArg = mockExecuteTask.firstCall.args[0] as Task;
582+
const execution = taskArg.execution as ShellExecution;
583+
assert.strictEqual(execution.command, 'uv', 'Should execute uv when uv mode is enabled');
584+
assert.deepStrictEqual(
585+
execution.args,
586+
['run', '--python', '/path/to/python', '-X', 'utf8', 'script.py'],
587+
'Should use run.executable as --python value and preserve run.args',
588+
);
589+
});
590+
591+
test('should use python literal under uv when execInfo is missing', async () => {
592+
// Mock - No execInfo at all; we fall back to the literal "python" and still run via uv
593+
const environment: PythonEnvironment = {
594+
envId: { id: 'test-env', managerId: 'test-manager' },
595+
name: 'Test Environment',
596+
displayName: 'Test Environment',
597+
displayPath: '/path/to/env',
598+
version: '3.9.0',
599+
environmentPath: Uri.file('/path/to/env'),
600+
sysPrefix: '/path/to/env',
601+
} as PythonEnvironment;
602+
603+
const options: PythonTaskExecutionOptions = {
604+
name: 'No ExecInfo UV Task',
605+
args: ['script.py'],
606+
};
607+
608+
mockGetWorkspaceFolder.returns(undefined);
609+
mockShouldUseUv.resolves(true);
610+
mockQuoteStringIfNecessary.withArgs('uv').returns('uv');
611+
mockExecuteTask.resolves({} as TaskExecution);
612+
613+
// Run
614+
await runAsTask(environment, options);
615+
616+
// Assert - warns about missing executable AND wraps the literal "python" under uv
617+
assert.ok(
618+
mockTraceWarn.calledWith('No Python executable found in environment; falling back to "python".'),
619+
'Should warn about missing executable',
620+
);
621+
const taskArg = mockExecuteTask.firstCall.args[0] as Task;
622+
const execution = taskArg.execution as ShellExecution;
623+
assert.strictEqual(execution.command, 'uv', 'Should execute uv even when execInfo is missing');
624+
assert.deepStrictEqual(
625+
execution.args,
626+
['run', '--python', 'python', 'script.py'],
627+
'Should pass the literal "python" fallback as the --python argument',
628+
);
629+
});
630+
631+
test('should preserve a Windows-style python path verbatim as --python argument under uv', async () => {
632+
// Mock - Windows backslash path; the python path now flows as a uv argument, not the executable
633+
const winPython = 'C:\\Users\\me\\.venv\\Scripts\\python.exe';
634+
const environment: PythonEnvironment = {
635+
envId: { id: 'test-env', managerId: 'test-manager' },
636+
name: 'Test Environment',
637+
displayName: 'Test Environment',
638+
displayPath: 'C:\\Users\\me\\.venv',
639+
version: '3.11.0',
640+
environmentPath: Uri.file(winPython),
641+
execInfo: {
642+
run: { executable: winPython, args: [] },
643+
},
644+
sysPrefix: 'C:\\Users\\me\\.venv',
645+
};
646+
647+
const options: PythonTaskExecutionOptions = {
648+
name: 'Windows UV Task',
649+
args: ['script.py'],
650+
};
651+
652+
mockGetWorkspaceFolder.returns(undefined);
653+
mockShouldUseUv.resolves(true);
654+
mockQuoteStringIfNecessary.withArgs('uv').returns('uv');
655+
mockExecuteTask.resolves({} as TaskExecution);
656+
657+
// Run
658+
await runAsTask(environment, options);
659+
660+
// Assert - the --python value matches the input path string (not quoted via quoteStringIfNecessary)
661+
const taskArg = mockExecuteTask.firstCall.args[0] as Task;
662+
const execution = taskArg.execution as ShellExecution;
663+
assert.strictEqual(execution.command, 'uv', 'Should execute uv when uv mode is enabled');
664+
assert.deepStrictEqual(
665+
execution.args,
666+
['run', '--python', winPython, 'script.py'],
667+
'Should preserve the Windows-style path verbatim as the --python value',
668+
);
669+
// quoteStringIfNecessary should not be called for the python path under uv (only for the executable)
670+
assert.ok(
671+
!mockQuoteStringIfNecessary.calledWith(winPython),
672+
'Should not quote the python path when it is a uv argument',
673+
);
674+
});
675+
676+
test('should append user args after env activated args under uv', async () => {
677+
// Mock - Env supplies activatedRun.args; ensure ordering: run --python <py> <env-args> <user-args>
678+
const environment: PythonEnvironment = {
679+
envId: { id: 'test-env', managerId: 'test-manager' },
680+
name: 'Test Environment',
681+
displayName: 'Test Environment',
682+
displayPath: '/path/to/env',
683+
version: '3.9.0',
684+
environmentPath: Uri.file('/path/to/env'),
685+
execInfo: {
686+
run: { executable: '/path/to/python', args: ['--default'] },
687+
activatedRun: {
688+
executable: '/activated/python',
689+
args: ['-X', 'utf8'],
690+
},
691+
},
692+
sysPrefix: '/path/to/env',
693+
};
694+
695+
const options: PythonTaskExecutionOptions = {
696+
name: 'Args Order UV Task',
697+
args: ['script.py', '--user-arg'],
698+
};
699+
700+
mockGetWorkspaceFolder.returns(undefined);
701+
mockShouldUseUv.resolves(true);
702+
mockQuoteStringIfNecessary.withArgs('uv').returns('uv');
703+
mockExecuteTask.resolves({} as TaskExecution);
704+
705+
// Run
706+
await runAsTask(environment, options);
707+
708+
// Assert
709+
const taskArg = mockExecuteTask.firstCall.args[0] as Task;
710+
const execution = taskArg.execution as ShellExecution;
711+
assert.deepStrictEqual(
712+
execution.args,
713+
['run', '--python', '/activated/python', '-X', 'utf8', 'script.py', '--user-arg'],
714+
'Env activated args should sit between --python and the user args',
715+
);
716+
});
717+
718+
test('should pass user args containing flags through to python under uv (regression guard)', async () => {
719+
// Mock - The run button only ever appends a file path, but API callers can pass arbitrary args.
720+
// This guards the contract that user args land after the script positional and are NOT consumed by uv.
721+
const environment: PythonEnvironment = {
722+
envId: { id: 'test-env', managerId: 'test-manager' },
723+
name: 'Test Environment',
724+
displayName: 'Test Environment',
725+
displayPath: '/path/to/env',
726+
version: '3.9.0',
727+
environmentPath: Uri.file('/path/to/env'),
728+
execInfo: {
729+
run: { executable: '/path/to/python', args: [] },
730+
},
731+
sysPrefix: '/path/to/env',
732+
};
733+
734+
const options: PythonTaskExecutionOptions = {
735+
name: 'Flag Args UV Task',
736+
args: ['script.py', '--user-flag', 'value'],
737+
};
738+
739+
mockGetWorkspaceFolder.returns(undefined);
740+
mockShouldUseUv.resolves(true);
741+
mockQuoteStringIfNecessary.withArgs('uv').returns('uv');
742+
mockExecuteTask.resolves({} as TaskExecution);
743+
744+
// Run
745+
await runAsTask(environment, options);
746+
747+
// Assert - the user flag appears after the script path (i.e. it goes to python, not uv).
748+
const taskArg = mockExecuteTask.firstCall.args[0] as Task;
749+
const execution = taskArg.execution as ShellExecution;
750+
assert.deepStrictEqual(
751+
execution.args,
752+
['run', '--python', '/path/to/python', 'script.py', '--user-flag', 'value'],
753+
'User args should be appended after --python <path> in the order provided',
754+
);
755+
});
756+
});
757+
479758
suite('Workspace Resolution', () => {
480759
test('should use workspace folder when project URI is provided', async () => {
481760
// Mock - Test workspace resolution

0 commit comments

Comments
 (0)