Files
openide/python/testSrc/com/jetbrains/env/PyExecutionFixtureTestTask.java
Vladimir Lagunov 0774cd8a14 Correct thread when creating SDK in PyExecutionFixtureTestTask
GitOrigin-RevId: 02b319981f6343c6303feee397c3a5de8cda1ea8
2023-05-19 16:40:57 +00:00

343 lines
13 KiB
Java

// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.env;
import com.intellij.execution.process.ProcessHandler;
import com.intellij.ide.util.projectWizard.EmptyModuleBuilder;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ActionsKt;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.application.WriteAction;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleType;
import com.intellij.openapi.module.ModuleTypeManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ex.ProjectManagerEx;
import com.intellij.openapi.projectRoots.ProjectJdkTable;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.newvfs.RefreshQueueImpl;
import com.intellij.testFramework.EdtTestUtil;
import com.intellij.testFramework.LightProjectDescriptor;
import com.intellij.testFramework.PsiTestUtil;
import com.intellij.testFramework.builders.ModuleFixtureBuilder;
import com.intellij.testFramework.fixtures.*;
import com.intellij.testFramework.fixtures.impl.ModuleFixtureBuilderImpl;
import com.intellij.testFramework.fixtures.impl.ModuleFixtureImpl;
import com.intellij.util.ExceptionUtil;
import com.intellij.util.ui.UIUtil;
import com.jetbrains.extensions.ModuleExtKt;
import com.jetbrains.python.PyNames;
import com.jetbrains.python.PythonModuleTypeBase;
import com.jetbrains.python.PythonTestUtil;
import com.jetbrains.python.packaging.PyCondaPackageManagerImpl;
import com.jetbrains.python.packaging.PyPackageManager;
import com.jetbrains.python.psi.LanguageLevel;
import com.jetbrains.python.sdk.InvalidSdkException;
import com.jetbrains.python.sdk.PythonSdkType;
import com.jetbrains.python.tools.sdkTools.PySdkTools;
import com.jetbrains.python.tools.sdkTools.SdkCreationType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.junit.Assert;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
/**
* <h1>Task to execute code using {@link CodeInsightTestFixture}</h1>
* <h2>How to use it</h2>
* <p>
* Each test may have some test data somewhere in VCS (like <strong>testData</strong> folder).
* It is called <strong>test data path</strong>.
* This task copies test data to some temporary location, and launches your test against it.
* To get this location, use {@link CodeInsightTestFixture#getTempDirFixture()}
* or {@link CodeInsightTestFixture#getTempDirPath()}.
* </p>
* <p>
* You provide path to test data using 2 parts: base ({@link #getTestDataPath()} and relative
* path as argument to {@link PyExecutionFixtureTestTask#PyExecutionFixtureTestTask(String)}.
* Path is merged then, and data copied to temporary location, available with {@link CodeInsightTestFixture#getTempDirFixture()}.
* You may provide <strong>null</strong> to argument if you do not want to copy anything.
* </p>
* <h2>Things to check to make sure you use this code correctly</h2>
* <ol>
* <li>
* You never access {@link #getTestDataPath()} or {@link CodeInsightTestFixture#getTempDirPath()} in tests.
* <strong>Always</strong> work with {@link CodeInsightTestFixture#getTempDirFixture()}
* </li>
* <li>
* When overwriting {@link #getTestDataPath()} you return path to your <strong>testData</strong> (see current impl.)
* </li>
* </ol>
*
* @author traff
* @author Ilya.Kazakevich
*/
public abstract class PyExecutionFixtureTestTask extends PyTestTask {
public static final int NORMAL_TIMEOUT = 30000;
public static final int LONG_TIMEOUT = 120000;
protected int myTimeout = NORMAL_TIMEOUT;
protected CodeInsightTestFixture myFixture;
@Nullable
private final String myRelativeTestDataPath;
/**
* @param relativeTestDataPath path that will be added to {@link #getTestDataPath()} to obtain test data path (the one
* that will be copied to temp folder. See class doc.).
* Pass null if you do not want to copy anything.
*/
protected PyExecutionFixtureTestTask(@Nullable final String relativeTestDataPath) {
myRelativeTestDataPath = relativeTestDataPath;
}
@Nullable
protected String getRelativeTestDataPath() {
return myRelativeTestDataPath;
}
/**
* Debug output of this classes will be captured and reported in case of test failure
*/
@NotNull
public Collection<Class<?>> getClassesToEnableDebug() {
return Collections.emptyList();
}
public Project getProject() {
return myFixture.getProject();
}
@Override
public void useNormalTimeout() {
myTimeout = NORMAL_TIMEOUT;
}
@Override
public void useLongTimeout() {
myTimeout = LONG_TIMEOUT;
}
/**
* Returns virt file by path. May be relative or not.
*
* @return file or null if file does not exist
*/
@Nullable
protected VirtualFile getFileByPath(@NotNull final String path) {
final File fileToWorkWith = new File(path);
return (fileToWorkWith.isAbsolute()
? LocalFileSystem.getInstance().findFileByIoFile(fileToWorkWith)
: myFixture.getTempDirFixture().getFile(path));
}
@Override
public void setUp(final String testName) throws Exception {
final IdeaTestFixtureFactory fixtureFactory = IdeaTestFixtureFactory.getFixtureFactory();
fixtureFactory.registerFixtureBuilder(MyModuleFixtureBuilder.class, MyModuleFixtureBuilderImpl.class);
final TestFixtureBuilder<IdeaProjectTestFixture> fixtureBuilder =
fixtureFactory.createFixtureBuilder(testName);
fixtureBuilder.addModule(MyModuleFixtureBuilder.class);
myFixture = fixtureFactory.createCodeInsightFixture(fixtureBuilder.getFixture());
myFixture.setTestDataPath(getTestDataPath());
myFixture.setUp();
final Module module = myFixture.getModule();
assert module != null;
PlatformPythonModuleType.ensureModuleRegistered();
if (StringUtil.isNotEmpty(myRelativeTestDataPath)) {
// Without performing the copy deliberately in the EDT, this code may stuck in a livelock for unclear reason.
EdtTestUtil.runInEdtAndWait(() -> myFixture.copyDirectoryToProject(myRelativeTestDataPath, ".").getPath());
}
final VirtualFile projectRoot = myFixture.getTempDirFixture().getFile(".");
PsiTestUtil.addSourceRoot(module, projectRoot);
PsiTestUtil.addContentRoot(module, projectRoot);
for (final String contentRoot : getContentRoots()) {
final VirtualFile file = myFixture.getTempDirFixture().getFile(contentRoot);
assert file != null && file.exists() : String.format("Content root does not exist %s", file);
PsiTestUtil.addContentRoot(module, file);
}
}
@NotNull
public LanguageLevel getLevelForSdk() {
return PythonSdkType.getLanguageLevelForSdk(ModuleExtKt.getSdk(myFixture.getModule()));
}
/**
* @return additional content roots
*/
@NotNull
protected List<String> getContentRoots() {
return new ArrayList<>();
}
protected String getFilePath(@NotNull final String path) {
final VirtualFile virtualFile = myFixture.getTempDirFixture().getFile(path);
assert virtualFile != null && virtualFile.exists() : String.format("No file '%s' in %s", path, myFixture.getTempDirPath());
return virtualFile.getPath();
}
/**
* @return root of your test data path on filesystem (this is base folder: class will add its relative path from ctor
* to create full path and copy it to temp folder, see class doc.)
*/
@NotNull
protected String getTestDataPath() {
return PythonTestUtil.getTestDataPath();
}
@Override
public void tearDown() throws Exception {
if (myFixture != null) {
EdtTestUtil.runInEdtAndWait(() -> {
UIUtil.dispatchAllInvocationEvents();
while (RefreshQueueImpl.isRefreshInProgress()) {
UIUtil.dispatchAllInvocationEvents();
}
for (Sdk sdk : ProjectJdkTable.getInstance().getSdksOfType(PythonSdkType.getInstance())) {
WriteAction.run(() -> {
if (sdk instanceof Disposable && !Disposer.isDisposed((Disposable)sdk)) {
ProjectJdkTable.getInstance().removeJdk(sdk);
}
});
}
});
// Teardown should be called on main thread because fixture teardown checks for
// thread leaks, and blocked main thread is considered as leaked
Project project = myFixture.getProject();
myFixture.tearDown();
if (project != null && !project.isDisposed()) {
ProjectManagerEx.getInstanceEx().forceCloseProject(project);
}
myFixture = null;
}
super.tearDown();
}
@Nullable
protected LightProjectDescriptor getProjectDescriptor() {
return null;
}
protected boolean waitFor(ProcessHandler p) {
return p.waitFor(myTimeout);
}
protected boolean waitFor(@NotNull Semaphore s) throws InterruptedException {
return waitFor(s, myTimeout);
}
protected static boolean waitFor(@NotNull Semaphore s, long timeout) throws InterruptedException {
return s.tryAcquire(timeout, TimeUnit.MILLISECONDS);
}
public static class MyModuleFixtureBuilderImpl extends ModuleFixtureBuilderImpl<ModuleFixture> implements MyModuleFixtureBuilder {
public MyModuleFixtureBuilderImpl(TestFixtureBuilder<? extends IdeaProjectTestFixture> fixtureBuilder) {
super(new PlatformPythonModuleType(), fixtureBuilder);
}
@NotNull
@Override
protected ModuleFixture instantiateFixture() {
return new ModuleFixtureImpl(this);
}
}
public static class PlatformPythonModuleType extends PythonModuleTypeBase<EmptyModuleBuilder> {
private static final String MODULE_ID = PyNames.PYTHON_MODULE_ID;
@NotNull
public static PlatformPythonModuleType getInstance() {
ensureModuleRegistered();
return (PlatformPythonModuleType)ModuleTypeManager.getInstance().findByID(PyNames.PYTHON_MODULE_ID);
}
static void ensureModuleRegistered() {
ModuleTypeManager moduleManager = ModuleTypeManager.getInstance();
if (!(moduleManager.findByID(MODULE_ID) instanceof PythonModuleTypeBase)) {
moduleManager.registerModuleType(new PlatformPythonModuleType());
}
}
@NotNull
@Override
public EmptyModuleBuilder createModuleBuilder() {
return new EmptyModuleBuilder() {
@Override
public ModuleType getModuleType() {
return getInstance();
}
};
}
}
/**
* Creates SDK by its path
*
* @param sdkHome path to sdk (probably obtained by {@link PyTestTask#runTestOn(String, Sdk)})
* @param sdkCreationType SDK creation strategy (see {@link sdkTools.SdkCreationType} doc)
* @return sdk
*/
@NotNull
protected Sdk createTempSdk(@NotNull final String sdkHome, @NotNull final SdkCreationType sdkCreationType)
throws InvalidSdkException {
final VirtualFile sdkHomeFile = LocalFileSystem.getInstance().findFileByPath(sdkHome);
Assert.assertNotNull("Interpreter file not found: " + sdkHome, sdkHomeFile);
CompletableFuture<Sdk> sdkRef = new CompletableFuture<>();
ApplicationManager.getApplication().invokeAndWait(() -> {
try {
sdkRef.complete(PySdkTools.createTempSdk(sdkHomeFile, sdkCreationType, myFixture.getModule(), myFixture.getTestRootDisposable()));
}
catch (InvalidSdkException e) {
sdkRef.completeExceptionally(e);
}
});
Sdk sdk;
try {
sdk = sdkRef.join();
}
catch (CompletionException err) {
if (err.getCause() instanceof InvalidSdkException cause) throw cause;
if (err.getCause() instanceof Error cause) throw cause;
if (err.getCause() instanceof RuntimeException cause) throw cause;
throw err;
}
// We use gradle script to create environment. This script utilizes Conda.
// Conda supports 2 types of package installation: conda native and pip. We use pip.
// PyCharm Conda support ignores packages installed via pip ("conda list -e" does it, see PyCondaPackageManagerImpl)
// So we need to either fix gradle (PythonEnvsPlugin.groovy on github) or use helper instead of "conda list" to get all packages
// We do the latter.
final PyPackageManager packageManager = PyPackageManager.getInstance(sdk);
if (packageManager instanceof PyCondaPackageManagerImpl) {
((PyCondaPackageManagerImpl)packageManager).useConda = false;
}
return sdk;
}
public interface MyModuleFixtureBuilder extends ModuleFixtureBuilder<ModuleFixture> {
}
}