Create test for virtual environment (#6007)

* Create test for virtual environment

* Figure out why python path test is failing

* Eliminate quotes on venv_path

* Actually set the python path

* Try just printing path on windows

* Print a different variable

* Try other echo patterns

* user powershell commands to append

* Fix prettier
This commit is contained in:
Rich Chiodo 2023-09-25 10:21:21 -07:00 committed by GitHub
parent 0e95e48650
commit 21aee77eb5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 830 additions and 564 deletions

View File

@ -2,6 +2,7 @@ name: 'Validation'
env:
NODE_VERSION: '16' # Shipped with VS Code.
PYTHON_VERSION: 3.11
on:
push:
@ -107,6 +108,48 @@ jobs:
run: npm test
working-directory: packages/pyright-internal
# Install python so we can create a VENV for tests
- name: Use Python ${{env.PYTHON_VERSION}}
uses: actions/setup-python@v4
id: install_python
with:
python-version: ${{env.PYTHON_VERSION}}
- name: Create Venv
run: |
${{ steps.install_python.outputs.python-path }} -m venv .venv
- name: Activate and install pytest (linux)
if: runner.os != 'Windows'
run: |
source .venv/bin/activate
python -m pip install pytest
python -c "import sys;print('python_venv_path=' + sys.executable)" >> $GITHUB_ENV
- name: Activate and install pytest (windows)
if: runner.os == 'Windows'
run: |
.venv\scripts\activate
python -m pip install pytest
python -c "import sys;print('python_venv_path=' + sys.executable)" | Out-File -FilePath $env:GITHUB_ENV -Append
- name: Echo python_venv_path
run: |
echo python_venv_path=${{env.python_venv_path}}
- name: Run import tests with venv
env:
CI_IMPORT_TEST_VENVPATH: '../../'
CI_IMPORT_TEST_VENV: '.venv'
run: npm run test:imports
working-directory: packages/pyright-internal
- name: Run import tests with pythonpath
env:
CI_IMPORT_TEST_PYTHONPATH: ${{env.python_venv_path}}
run: npm run test:imports
working-directory: packages/pyright-internal
build:
runs-on: ubuntu-latest
name: Build

3
.gitignore vendored
View File

@ -123,3 +123,6 @@ junit.xml
# OS files
.DS_Store
Thumbs.db
# Potential venv
.venv

View File

@ -12,7 +12,8 @@
"build": "tsc",
"clean": "shx rm -rf ./dist ./out",
"test": "jest --forceExit",
"test:coverage": "jest --forceExit --reporters=jest-junit --reporters=default --coverage --coverageReporters=cobertura --coverageReporters=html --coverageReporters=json"
"test:coverage": "jest --forceExit --reporters=jest-junit --reporters=default --coverage --coverageReporters=cobertura --coverageReporters=html --coverageReporters=json",
"test:imports": "jest importResolver.test --forceExit --runInBand"
},
"dependencies": {
"@iarna/toml": "2.2.5",

View File

@ -6,18 +6,30 @@
import assert from 'assert';
import { Dirent, ReadStream, WriteStream } from 'fs';
import { ImportResolver } from '../analyzer/importResolver';
import { ConfigOptions } from '../common/configOptions';
import { FileSystem, MkDirOptions, Stats, TmpfileOptions } from '../common/fileSystem';
import { FileWatcher, FileWatcherEventHandler } from '../common/fileWatcher';
import { FullAccessHost } from '../common/fullAccessHost';
import { Host } from '../common/host';
import { lib, sitePackages, typeshedFallback } from '../common/pathConsts';
import { combinePaths, getDirectoryPath, normalizeSlashes } from '../common/pathUtils';
import { createFromRealFileSystem } from '../common/realFileSystem';
import { ServiceProvider } from '../common/serviceProvider';
import { createServiceProvider } from '../common/serviceProviderExtensions';
import { PyrightFileSystem } from '../pyrightFileSystem';
import { TestAccessHost } from './harness/testAccessHost';
import { TestFileSystem } from './harness/vfs/filesystem';
import { ServiceProvider } from '../common/serviceProvider';
import { createServiceProvider } from '../common/serviceProviderExtensions';
const libraryRoot = combinePaths(normalizeSlashes('/'), lib, sitePackages);
function usingTrueVenv() {
return process.env.CI_IMPORT_TEST_VENVPATH !== undefined || process.env.CI_IMPORT_TEST_PYTHONPATH !== undefined;
}
if (!usingTrueVenv()) {
describe('Import tests that cannot run in a true venv', () => {
test('partial stub file exists', () => {
const files = [
{
@ -39,7 +51,8 @@ test('partial stub file exists', () => {
assert(importResult.isStubFile);
assert.strictEqual(
1,
importResult.resolvedPaths.filter((f) => f === combinePaths(libraryRoot, 'myLib', 'partialStub.pyi')).length
importResult.resolvedPaths.filter((f) => f === combinePaths(libraryRoot, 'myLib', 'partialStub.pyi'))
.length
);
});
@ -64,10 +77,187 @@ test('partial stub __init__ exists', () => {
assert(importResult.isStubFile);
assert.strictEqual(
1,
importResult.resolvedPaths.filter((f) => f === combinePaths(libraryRoot, 'myLib', '__init__.pyi')).length
importResult.resolvedPaths.filter((f) => f === combinePaths(libraryRoot, 'myLib', '__init__.pyi'))
.length
);
});
test('stub package', () => {
const files = [
{
path: combinePaths(libraryRoot, 'myLib-stubs', 'stub.pyi'),
content: '# empty',
},
{
path: combinePaths(libraryRoot, 'myLib-stubs', '__init__.pyi'),
content: '# empty',
},
{
path: combinePaths(libraryRoot, 'myLib', 'partialStub.py'),
content: 'def test(): pass',
},
];
// If fully typed stub package exists, that wins over the real package.
const importResult = getImportResult(files, ['myLib', 'partialStub']);
assert(!importResult.isImportFound);
});
test('partial stub package in typing folder', () => {
const typingFolder = combinePaths(normalizeSlashes('/'), 'typing');
const files = [
{
path: combinePaths(typingFolder, 'myLib-stubs', '__init__.pyi'),
content: 'def test(): ...',
},
{
path: combinePaths(typingFolder, 'myLib-stubs', 'py.typed'),
content: 'partial\n',
},
{
path: combinePaths(libraryRoot, 'myLib', '__init__.py'),
content: 'def test(): pass',
},
];
const importResult = getImportResult(files, ['myLib'], (c) => (c.stubPath = typingFolder));
assert(importResult.isImportFound);
assert(importResult.isStubFile);
assert.strictEqual(
1,
importResult.resolvedPaths.filter((f) => f === combinePaths(libraryRoot, 'myLib', '__init__.pyi'))
.length
);
});
test('typeshed folder', () => {
const typeshedFolder = combinePaths(normalizeSlashes('/'), 'ts');
const files = [
{
path: combinePaths(libraryRoot, 'myLib-stubs', '__init__.pyi'),
content: 'def test(): ...',
},
{
path: combinePaths(libraryRoot, 'myLib-stubs', 'py.typed'),
content: 'partial\n',
},
{
path: combinePaths(typeshedFolder, 'stubs', 'myLibPackage', 'myLib.pyi'),
content: '# empty',
},
{
path: combinePaths(libraryRoot, 'myLib', '__init__.py'),
content: 'def test(): pass',
},
];
// Stub packages win over typeshed.
const importResult = getImportResult(files, ['myLib'], (c) => (c.typeshedPath = typeshedFolder));
assert(importResult.isImportFound);
assert(importResult.isStubFile);
assert.strictEqual(
1,
importResult.resolvedPaths.filter((f) => f === combinePaths(libraryRoot, 'myLib', '__init__.pyi'))
.length
);
});
test('typeshed fallback folder', () => {
const files = [
{
path: combinePaths(libraryRoot, 'myLib-stubs', '__init__.pyi'),
content: 'def test(): ...',
},
{
path: combinePaths(libraryRoot, 'myLib-stubs', 'py.typed'),
content: 'partial\n',
},
{
path: combinePaths('/', typeshedFallback, 'stubs', 'myLibPackage', 'myLib.pyi'),
content: '# empty',
},
{
path: combinePaths(libraryRoot, 'myLib', '__init__.py'),
content: 'def test(): pass',
},
];
// Stub packages win over typeshed.
const importResult = getImportResult(files, ['myLib']);
assert(importResult.isImportFound);
assert(importResult.isStubFile);
assert.strictEqual(
1,
importResult.resolvedPaths.filter((f) => f === combinePaths(libraryRoot, 'myLib', '__init__.pyi'))
.length
);
});
test('py.typed file', () => {
const files = [
{
path: combinePaths(libraryRoot, 'myLib-stubs', '__init__.pyi'),
content: 'def test(): ...',
},
{
path: combinePaths(libraryRoot, 'myLib-stubs', 'py.typed'),
content: 'partial\n',
},
{
path: combinePaths(libraryRoot, 'myLib', '__init__.py'),
content: 'def test(): pass',
},
{
path: combinePaths(libraryRoot, 'myLib', 'py.typed'),
content: '# typed',
},
];
// Partial stub package always overrides original package.
const importResult = getImportResult(files, ['myLib']);
assert(importResult.isImportFound);
assert(importResult.isStubFile);
});
test('py.typed library', () => {
const files = [
{
path: combinePaths(libraryRoot, 'os', '__init__.py'),
content: 'def test(): ...',
},
{
path: combinePaths(libraryRoot, 'os', 'py.typed'),
content: '',
},
{
path: combinePaths('/', typeshedFallback, 'stubs', 'os', 'os', '__init__.pyi'),
content: '# empty',
},
];
const importResult = getImportResult(files, ['os']);
assert(importResult.isImportFound);
assert.strictEqual(files[0].path, importResult.resolvedPaths[importResult.resolvedPaths.length - 1]);
});
test('import side by side file sub under lib folder', () => {
const files = [
{
path: combinePaths('/lib/site-packages/myLib', 'file1.py'),
content: 'def test1(): ...',
},
{
path: combinePaths('/lib/site-packages/myLib', 'file2.py'),
content: 'def test2(): ...',
},
];
const importResult = getImportResult(files, ['file1']);
assert(!importResult.isImportFound);
});
});
}
describe('Import tests that can run with or without a true venv', () => {
test('side by side files', () => {
const myFile = combinePaths('src', 'file.py');
const files = [
@ -137,27 +327,6 @@ test('side by side files', () => {
assert.strictEqual(1, partialStubResult.resolvedPaths.filter((f) => f === partialStubFile).length);
});
test('stub package', () => {
const files = [
{
path: combinePaths(libraryRoot, 'myLib-stubs', 'stub.pyi'),
content: '# empty',
},
{
path: combinePaths(libraryRoot, 'myLib-stubs', '__init__.pyi'),
content: '# empty',
},
{
path: combinePaths(libraryRoot, 'myLib', 'partialStub.py'),
content: 'def test(): pass',
},
];
// If fully typed stub package exists, that wins over the real package.
const importResult = getImportResult(files, ['myLib', 'partialStub']);
assert(!importResult.isImportFound);
});
test('stub namespace package', () => {
const files = [
{
@ -211,140 +380,6 @@ test('stub in typing folder over partial stub package', () => {
);
});
test('partial stub package in typing folder', () => {
const typingFolder = combinePaths(normalizeSlashes('/'), 'typing');
const files = [
{
path: combinePaths(typingFolder, 'myLib-stubs', '__init__.pyi'),
content: 'def test(): ...',
},
{
path: combinePaths(typingFolder, 'myLib-stubs', 'py.typed'),
content: 'partial\n',
},
{
path: combinePaths(libraryRoot, 'myLib', '__init__.py'),
content: 'def test(): pass',
},
];
const importResult = getImportResult(files, ['myLib'], (c) => (c.stubPath = typingFolder));
assert(importResult.isImportFound);
assert(importResult.isStubFile);
assert.strictEqual(
1,
importResult.resolvedPaths.filter((f) => f === combinePaths(libraryRoot, 'myLib', '__init__.pyi')).length
);
});
test('typeshed folder', () => {
const typeshedFolder = combinePaths(normalizeSlashes('/'), 'ts');
const files = [
{
path: combinePaths(libraryRoot, 'myLib-stubs', '__init__.pyi'),
content: 'def test(): ...',
},
{
path: combinePaths(libraryRoot, 'myLib-stubs', 'py.typed'),
content: 'partial\n',
},
{
path: combinePaths(typeshedFolder, 'stubs', 'myLibPackage', 'myLib.pyi'),
content: '# empty',
},
{
path: combinePaths(libraryRoot, 'myLib', '__init__.py'),
content: 'def test(): pass',
},
];
// Stub packages win over typeshed.
const importResult = getImportResult(files, ['myLib'], (c) => (c.typeshedPath = typeshedFolder));
assert(importResult.isImportFound);
assert(importResult.isStubFile);
assert.strictEqual(
1,
importResult.resolvedPaths.filter((f) => f === combinePaths(libraryRoot, 'myLib', '__init__.pyi')).length
);
});
test('typeshed fallback folder', () => {
const files = [
{
path: combinePaths(libraryRoot, 'myLib-stubs', '__init__.pyi'),
content: 'def test(): ...',
},
{
path: combinePaths(libraryRoot, 'myLib-stubs', 'py.typed'),
content: 'partial\n',
},
{
path: combinePaths('/', typeshedFallback, 'stubs', 'myLibPackage', 'myLib.pyi'),
content: '# empty',
},
{
path: combinePaths(libraryRoot, 'myLib', '__init__.py'),
content: 'def test(): pass',
},
];
// Stub packages win over typeshed.
const importResult = getImportResult(files, ['myLib']);
assert(importResult.isImportFound);
assert(importResult.isStubFile);
assert.strictEqual(
1,
importResult.resolvedPaths.filter((f) => f === combinePaths(libraryRoot, 'myLib', '__init__.pyi')).length
);
});
test('py.typed file', () => {
const files = [
{
path: combinePaths(libraryRoot, 'myLib-stubs', '__init__.pyi'),
content: 'def test(): ...',
},
{
path: combinePaths(libraryRoot, 'myLib-stubs', 'py.typed'),
content: 'partial\n',
},
{
path: combinePaths(libraryRoot, 'myLib', '__init__.py'),
content: 'def test(): pass',
},
{
path: combinePaths(libraryRoot, 'myLib', 'py.typed'),
content: '# typed',
},
];
// Partial stub package always overrides original package.
const importResult = getImportResult(files, ['myLib']);
assert(importResult.isImportFound);
assert(importResult.isStubFile);
});
test('py.typed library', () => {
const files = [
{
path: combinePaths(libraryRoot, 'os', '__init__.py'),
content: 'def test(): ...',
},
{
path: combinePaths(libraryRoot, 'os', 'py.typed'),
content: '',
},
{
path: combinePaths('/', typeshedFallback, 'stubs', 'os', 'os', '__init__.pyi'),
content: '# empty',
},
];
const importResult = getImportResult(files, ['os']);
assert(importResult.isImportFound);
assert.strictEqual(files[0].path, importResult.resolvedPaths[importResult.resolvedPaths.length - 1]);
});
test('non py.typed library', () => {
const files = [
{
@ -394,8 +429,14 @@ test('multiple typeshedFallback', () => {
);
const importRoots = importResolver.getImportRoots(configOptions.getDefaultExecEnvironment());
assert.strictEqual(1, importRoots.filter((f) => f === combinePaths('/', typeshedFallback, 'stubs', 'aLib')).length);
assert.strictEqual(1, importRoots.filter((f) => f === combinePaths('/', typeshedFallback, 'stubs', 'bLib')).length);
assert.strictEqual(
1,
importRoots.filter((f) => f === combinePaths('/', typeshedFallback, 'stubs', 'aLib')).length
);
assert.strictEqual(
1,
importRoots.filter((f) => f === combinePaths('/', typeshedFallback, 'stubs', 'bLib')).length
);
});
test('import side by side file root', () => {
@ -472,22 +513,6 @@ test('import file sub under containing folder', () => {
);
});
test('import side by side file sub under lib folder', () => {
const files = [
{
path: combinePaths('/lib/site-packages/myLib', 'file1.py'),
content: 'def test1(): ...',
},
{
path: combinePaths('/lib/site-packages/myLib', 'file2.py'),
content: 'def test2(): ...',
},
];
const importResult = getImportResult(files, ['file1']);
assert(!importResult.isImportFound);
});
test("don't walk up the root", () => {
const files = [
{
@ -587,21 +612,72 @@ test('nested namespace package 4', () => {
});
assert(!importResult.isImportFound);
});
});
if (usingTrueVenv()) {
describe('Import tests that have to run with a venv', () => {
test('venv can find imports', () => {
const files = [
{
path: combinePaths('/', 'file1.py'),
content: 'import pytest',
},
];
const importResult = getImportResult(files, ['pytest']);
assert(importResult.isImportFound, `Import not found: ${importResult.importFailureInfo?.join('\n')}`);
});
});
}
function getImportResult(
files: { path: string; content: string }[],
nameParts: string[],
setup?: (c: ConfigOptions) => void
) {
setup =
const defaultHostFactory = (sp: ServiceProvider) => new TestAccessHost(sp.fs().getModulePath(), [libraryRoot]);
const defaultSetup =
setup ??
((c) => {
/* empty */
});
const defaultSpFactory = (files: { path: string; content: string }[]) => createServiceProviderFromFiles(files);
const sp = createServiceProviderFromFiles(files);
// Use environment variables to determine how to create a host and how to modify the config options.
// These are set in the CI to test imports with different options.
let hostFactory: (sp: ServiceProvider) => Host = defaultHostFactory;
let configModifier = defaultSetup;
let spFactory = defaultSpFactory;
if (process.env.CI_IMPORT_TEST_VENVPATH) {
configModifier = (c: ConfigOptions) => {
defaultSetup(c);
c.venvPath = process.env.CI_IMPORT_TEST_VENVPATH;
c.venv = process.env.CI_IMPORT_TEST_VENV;
};
spFactory = (files: { path: string; content: string }[]) => createServiceProviderWithCombinedFs(files);
} else if (process.env.CI_IMPORT_TEST_PYTHONPATH) {
configModifier = (c: ConfigOptions) => {
defaultSetup(c);
c.pythonPath = process.env.CI_IMPORT_TEST_PYTHONPATH;
};
hostFactory = (sp: ServiceProvider) => new TruePythonTestAccessHost();
spFactory = (files: { path: string; content: string }[]) => createServiceProviderWithCombinedFs(files);
}
return getImportResultImpl(files, nameParts, spFactory, configModifier, hostFactory);
}
function getImportResultImpl(
files: { path: string; content: string }[],
nameParts: string[],
spFactory: (files: { path: string; content: string }[]) => ServiceProvider,
configModifier: (c: ConfigOptions) => void,
hostFactory: (sp: ServiceProvider) => Host
) {
const sp = spFactory(files);
const configOptions = new ConfigOptions(normalizeSlashes('/'));
setup(configOptions);
configModifier(configOptions);
const file = files.length > 0 ? files[files.length - 1].path : combinePaths('src', 'file.py');
if (files.length === 0) {
@ -611,11 +687,7 @@ function getImportResult(
});
}
const importResolver = new ImportResolver(
sp,
configOptions,
new TestAccessHost(sp.fs().getModulePath(), [libraryRoot])
);
const importResolver = new ImportResolver(sp, configOptions, hostFactory(sp));
const importResult = importResolver.resolveImport(file, configOptions.findExecEnvironment(file), {
leadingDots: 0,
nameParts: nameParts,
@ -624,8 +696,7 @@ function getImportResult(
return importResult;
}
function createFileSystem(files: { path: string; content: string }[]): PyrightFileSystem {
function createTestFileSystem(files: { path: string; content: string }[]): TestFileSystem {
const fs = new TestFileSystem(/* ignoreCase */ false, { cwd: normalizeSlashes('/') });
for (const file of files) {
@ -636,10 +707,158 @@ function createFileSystem(files: { path: string; content: string }[]): PyrightFi
fs.writeFileSync(path, file.content);
}
return new PyrightFileSystem(fs);
return fs;
}
function createServiceProviderFromFiles(files: { path: string; content: string }[]): ServiceProvider {
const fs = createFileSystem(files);
const fs = new PyrightFileSystem(createTestFileSystem(files));
return createServiceProvider(fs);
}
function createServiceProviderWithCombinedFs(files: { path: string; content: string }[]): ServiceProvider {
const fs = new PyrightFileSystem(new CombinedFileSystem(createTestFileSystem(files)));
return createServiceProvider(fs);
}
class TruePythonTestAccessHost extends FullAccessHost {
constructor() {
super(createFromRealFileSystem());
}
}
class CombinedFileSystem implements FileSystem {
private _realFS = createFromRealFileSystem();
constructor(private _testFS: FileSystem) {}
mkdirSync(path: string, options?: MkDirOptions | undefined): void {
this._testFS.mkdirSync(path, options);
}
writeFileSync(path: string, data: string | Buffer, encoding: BufferEncoding | null): void {
this._testFS.writeFileSync(path, data, encoding);
}
unlinkSync(path: string): void {
this._testFS.unlinkSync(path);
}
rmdirSync(path: string): void {
this._testFS.rmdirSync(path);
}
createFileSystemWatcher(paths: string[], listener: FileWatcherEventHandler): FileWatcher {
return this._testFS.createFileSystemWatcher(paths, listener);
}
createReadStream(path: string): ReadStream {
return this._testFS.createReadStream(path);
}
createWriteStream(path: string): WriteStream {
return this._testFS.createWriteStream(path);
}
copyFileSync(src: string, dst: string): void {
this._testFS.copyFileSync(src, dst);
}
tmpdir(): string {
return this._testFS.tmpdir();
}
tmpfile(options?: TmpfileOptions | undefined): string {
return this._testFS.tmpfile(options);
}
dispose(): void {
this._testFS.dispose();
}
existsSync(path: string): boolean {
return this._testFS.existsSync(path) || this._realFS.existsSync(path);
}
chdir(path: string): void {
this._testFS.chdir(path);
}
readdirEntriesSync(path: string): Dirent[] {
if (this._testFS.existsSync(path)) {
return this._testFS.readdirEntriesSync(path);
}
return this._realFS.readdirEntriesSync(path);
}
readdirSync(path: string): string[] {
if (this._testFS.existsSync(path)) {
return this._testFS.readdirSync(path);
}
return this._realFS.readdirSync(path);
}
readFileSync(path: string, encoding?: null): Buffer;
readFileSync(path: string, encoding: BufferEncoding): string;
readFileSync(path: string, encoding?: BufferEncoding | null): string | Buffer;
readFileSync(path: string, encoding: BufferEncoding | null = null) {
if (this._testFS.existsSync(path)) {
return this._testFS.readFileSync(path, encoding);
}
return this._realFS.readFileSync(path, encoding);
}
statSync(path: string): Stats {
if (this._testFS.existsSync(path)) {
return this._testFS.statSync(path);
}
return this._realFS.statSync(path);
}
realpathSync(path: string): string {
if (this._testFS.existsSync(path)) {
return this._testFS.realpathSync(path);
}
return this._realFS.realpathSync(path);
}
getModulePath(): string {
return this._testFS.getModulePath();
}
readFile(path: string): Promise<Buffer> {
if (this._testFS.existsSync(path)) {
return this._testFS.readFile(path);
}
return this._realFS.readFile(path);
}
readFileText(path: string, encoding?: BufferEncoding | undefined): Promise<string> {
if (this._testFS.existsSync(path)) {
return this._testFS.readFileText(path, encoding);
}
return this._realFS.readFileText(path, encoding);
}
realCasePath(path: string): string {
return this._testFS.realCasePath(path);
}
isMappedFilePath(filepath: string): boolean {
return this._testFS.isMappedFilePath(filepath);
}
getOriginalFilePath(mappedFilePath: string): string {
return this._testFS.getOriginalFilePath(mappedFilePath);
}
getMappedFilePath(originalFilepath: string): string {
return this._testFS.getMappedFilePath(originalFilepath);
}
getUri(path: string): string {
return this._testFS.getUri(path);
}
isInZip(path: string): boolean {
return this._testFS.isInZip(path);
}
}