mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-03-22 15:19:59 +07:00
2443 lines
100 KiB
Java
2443 lines
100 KiB
Java
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
|
package com.intellij.codeInsight.daemon.impl;
|
|
|
|
import com.intellij.application.options.editor.CodeFoldingConfigurable;
|
|
import com.intellij.codeHighlighting.*;
|
|
import com.intellij.codeHighlighting.Pass;
|
|
import com.intellij.codeInsight.EditorInfo;
|
|
import com.intellij.codeInsight.daemon.DaemonAnalyzerTestCase;
|
|
import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer;
|
|
import com.intellij.codeInsight.daemon.DaemonCodeAnalyzerSettings;
|
|
import com.intellij.codeInsight.daemon.HighlightDisplayKey;
|
|
import com.intellij.codeInsight.daemon.impl.analysis.FileHighlightingSetting;
|
|
import com.intellij.codeInsight.daemon.impl.analysis.HighlightingSettingsPerFile;
|
|
import com.intellij.codeInsight.daemon.quickFix.LightQuickFixTestCase;
|
|
import com.intellij.codeInsight.folding.JavaCodeFoldingSettings;
|
|
import com.intellij.codeInsight.intention.IntentionAction;
|
|
import com.intellij.codeInspection.InspectionProfile;
|
|
import com.intellij.codeInspection.LocalInspectionTool;
|
|
import com.intellij.codeInspection.accessStaticViaInstance.AccessStaticViaInstance;
|
|
import com.intellij.codeInspection.deadCode.UnusedDeclarationInspection;
|
|
import com.intellij.codeInspection.deadCode.UnusedDeclarationInspectionBase;
|
|
import com.intellij.codeInspection.ex.InspectionProfileImpl;
|
|
import com.intellij.codeInspection.htmlInspections.RequiredAttributesInspectionBase;
|
|
import com.intellij.codeInspection.unusedSymbol.UnusedSymbolLocalInspection;
|
|
import com.intellij.codeInspection.varScopeCanBeNarrowed.FieldCanBeLocalInspection;
|
|
import com.intellij.concurrency.ConcurrentCollectionFactory;
|
|
import com.intellij.configurationStore.StorageUtilKt;
|
|
import com.intellij.configurationStore.StoreUtil;
|
|
import com.intellij.configurationStore.StoreUtilKt;
|
|
import com.intellij.diagnostic.ThreadDumper;
|
|
import com.intellij.execution.filters.TextConsoleBuilderFactory;
|
|
import com.intellij.execution.ui.ConsoleView;
|
|
import com.intellij.execution.ui.ConsoleViewContentType;
|
|
import com.intellij.ide.GeneralSettings;
|
|
import com.intellij.ide.highlighter.JavaFileType;
|
|
import com.intellij.ide.highlighter.XmlFileType;
|
|
import com.intellij.javaee.ExternalResourceManagerExImpl;
|
|
import com.intellij.lang.LanguageFilter;
|
|
import com.intellij.lang.annotation.AnnotationHolder;
|
|
import com.intellij.lang.annotation.HighlightSeverity;
|
|
import com.intellij.lang.xml.XMLLanguage;
|
|
import com.intellij.openapi.Disposable;
|
|
import com.intellij.openapi.actionSystem.CommonDataKeys;
|
|
import com.intellij.openapi.actionSystem.DataContext;
|
|
import com.intellij.openapi.actionSystem.IdeActions;
|
|
import com.intellij.openapi.actionSystem.impl.SimpleDataContext;
|
|
import com.intellij.openapi.application.ApplicationManager;
|
|
import com.intellij.openapi.application.WriteAction;
|
|
import com.intellij.openapi.command.CommandProcessor;
|
|
import com.intellij.openapi.command.WriteCommandAction;
|
|
import com.intellij.openapi.command.undo.UndoManager;
|
|
import com.intellij.openapi.editor.*;
|
|
import com.intellij.openapi.editor.actionSystem.EditorActionManager;
|
|
import com.intellij.openapi.editor.actionSystem.TypedAction;
|
|
import com.intellij.openapi.editor.ex.EditorEx;
|
|
import com.intellij.openapi.editor.ex.MarkupModelEx;
|
|
import com.intellij.openapi.editor.ex.RangeHighlighterEx;
|
|
import com.intellij.openapi.editor.impl.DocumentMarkupModel;
|
|
import com.intellij.openapi.editor.impl.EditorImpl;
|
|
import com.intellij.openapi.editor.impl.event.MarkupModelListener;
|
|
import com.intellij.openapi.editor.markup.MarkupModel;
|
|
import com.intellij.openapi.editor.markup.RangeHighlighter;
|
|
import com.intellij.openapi.fileEditor.*;
|
|
import com.intellij.openapi.fileEditor.impl.text.PsiAwareTextEditorProvider;
|
|
import com.intellij.openapi.fileEditor.impl.text.TextEditorProvider;
|
|
import com.intellij.openapi.fileTypes.PlainTextFileType;
|
|
import com.intellij.openapi.module.Module;
|
|
import com.intellij.openapi.progress.ProcessCanceledException;
|
|
import com.intellij.openapi.progress.ProgressIndicator;
|
|
import com.intellij.openapi.project.*;
|
|
import com.intellij.openapi.projectRoots.Sdk;
|
|
import com.intellij.openapi.projectRoots.impl.JavaAwareProjectJdkTableImpl;
|
|
import com.intellij.openapi.util.*;
|
|
import com.intellij.openapi.util.text.StringUtil;
|
|
import com.intellij.openapi.vfs.VirtualFile;
|
|
import com.intellij.pom.java.LanguageLevel;
|
|
import com.intellij.profile.codeInspection.InspectionProfileManager;
|
|
import com.intellij.profile.codeInspection.InspectionProjectProfileManager;
|
|
import com.intellij.psi.*;
|
|
import com.intellij.psi.search.GlobalSearchScope;
|
|
import com.intellij.refactoring.inline.InlineRefactoringActionHandler;
|
|
import com.intellij.testFramework.*;
|
|
import com.intellij.testFramework.fixtures.impl.CodeInsightTestFixtureImpl;
|
|
import com.intellij.util.*;
|
|
import com.intellij.util.concurrency.AppExecutorUtil;
|
|
import com.intellij.util.concurrency.ThreadingAssertions;
|
|
import com.intellij.util.containers.ContainerUtil;
|
|
import com.intellij.util.io.storage.HeavyProcessLatch;
|
|
import com.intellij.util.ui.UIUtil;
|
|
import com.intellij.xml.util.CheckDtdReferencesInspection;
|
|
import kotlin.Unit;
|
|
import org.intellij.lang.annotations.Language;
|
|
import org.jetbrains.annotations.NonNls;
|
|
import org.jetbrains.annotations.NotNull;
|
|
import org.jetbrains.annotations.Nullable;
|
|
|
|
import java.awt.*;
|
|
import java.io.File;
|
|
import java.io.IOException;
|
|
import java.util.*;
|
|
import java.util.List;
|
|
import java.util.concurrent.ExecutionException;
|
|
import java.util.concurrent.Future;
|
|
import java.util.concurrent.TimeUnit;
|
|
import java.util.concurrent.TimeoutException;
|
|
import java.util.concurrent.atomic.AtomicBoolean;
|
|
import java.util.concurrent.atomic.AtomicInteger;
|
|
import java.util.concurrent.atomic.AtomicReference;
|
|
import java.util.stream.Collectors;
|
|
|
|
/**
|
|
* tests general daemon behaviour/interruptibility/restart during highlighting
|
|
*/
|
|
@SkipSlowTestLocally
|
|
@DaemonAnalyzerTestCase.CanChangeDocumentDuringHighlighting
|
|
public class DaemonRespondToChangesTest extends DaemonAnalyzerTestCase {
|
|
public static final String BASE_PATH = "/codeInsight/daemonCodeAnalyzer/typing/";
|
|
|
|
private DaemonCodeAnalyzerImpl myDaemonCodeAnalyzer;
|
|
|
|
@Override
|
|
protected void setUp() throws Exception {
|
|
super.setUp();
|
|
enableInspectionTool(new UnusedDeclarationInspection());
|
|
myDaemonCodeAnalyzer = (DaemonCodeAnalyzerImpl)DaemonCodeAnalyzer.getInstance(getProject());
|
|
UndoManager.getInstance(myProject);
|
|
myDaemonCodeAnalyzer.setUpdateByTimerEnabled(true);
|
|
}
|
|
|
|
@Override
|
|
protected void tearDown() throws Exception {
|
|
try {
|
|
if (myEditor != null) {
|
|
Document document = myEditor.getDocument();
|
|
FileDocumentManager.getInstance().reloadFromDisk(document);
|
|
}
|
|
Project project = getProject();
|
|
if (project != null) {
|
|
doPostponedFormatting(project);
|
|
}
|
|
}
|
|
catch (Throwable e) {
|
|
addSuppressedException(e);
|
|
}
|
|
finally {
|
|
myDaemonCodeAnalyzer = null;
|
|
super.tearDown();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void runTestRunnable(@NotNull ThrowableRunnable<Throwable> testRunnable) throws Throwable {
|
|
DaemonProgressIndicator.runInDebugMode(() -> super.runTestRunnable(testRunnable));
|
|
}
|
|
|
|
@Override
|
|
protected Sdk getTestProjectJdk() {
|
|
//noinspection removal
|
|
return JavaAwareProjectJdkTableImpl.getInstanceEx().getInternalJdk();
|
|
}
|
|
|
|
@Override
|
|
protected @NotNull LanguageLevel getProjectLanguageLevel() {
|
|
return LanguageLevel.JDK_11;
|
|
}
|
|
|
|
@Override
|
|
protected void configureByExistingFile(@NotNull VirtualFile virtualFile) {
|
|
super.configureByExistingFile(virtualFile);
|
|
setActiveEditors(getEditor());
|
|
}
|
|
|
|
@Override
|
|
protected VirtualFile configureByFiles(@Nullable File rawProjectRoot, VirtualFile @NotNull ... vFiles) throws IOException {
|
|
VirtualFile file = super.configureByFiles(rawProjectRoot, vFiles);
|
|
setActiveEditors(getEditor());
|
|
return file;
|
|
}
|
|
|
|
private void setActiveEditors(Editor @NotNull ... editors) {
|
|
(EditorTracker.Companion.getInstance(myProject)).setActiveEditors(Arrays.asList(editors));
|
|
}
|
|
|
|
@Override
|
|
protected boolean doTestLineMarkers() {
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
protected LocalInspectionTool[] configureLocalInspectionTools() {
|
|
return new LocalInspectionTool[] {
|
|
new FieldCanBeLocalInspection(),
|
|
new RequiredAttributesInspectionBase(),
|
|
new CheckDtdReferencesInspection(),
|
|
new AccessStaticViaInstance(),
|
|
};
|
|
}
|
|
|
|
@Override
|
|
protected void setUpProject() throws Exception {
|
|
super.setUpProject();
|
|
// treat listeners added there as not leaks
|
|
EditorMouseHoverPopupManager.getInstance();
|
|
}
|
|
|
|
private static void typeInAlienEditor(@NotNull Editor alienEditor, char c) {
|
|
EditorActionManager.getInstance();
|
|
TypedAction action = TypedAction.getInstance();
|
|
DataContext dataContext = ((EditorEx)alienEditor).getDataContext();
|
|
|
|
action.actionPerformed(alienEditor, c, dataContext);
|
|
}
|
|
|
|
public void testTypingSpaceInsideError() {
|
|
configureByText(JavaFileType.INSTANCE, """
|
|
class AClass {
|
|
{
|
|
toString(0,<caret>0);
|
|
}
|
|
}
|
|
""");
|
|
assertOneElement(highlightErrors());
|
|
|
|
for (int i = 0; i < 100; i++) {
|
|
type(" ");
|
|
assertOneElement(highlightErrors());
|
|
}
|
|
}
|
|
|
|
|
|
public void testBackSpaceInsideError() {
|
|
configureByText(JavaFileType.INSTANCE, """
|
|
class E {
|
|
void fff() {
|
|
int i = <caret>
|
|
}
|
|
}
|
|
""");
|
|
assertOneElement(highlightErrors());
|
|
|
|
backspace();
|
|
assertOneElement(highlightErrors());
|
|
}
|
|
|
|
public void testUnusedFieldUpdate() {
|
|
configureByText(JavaFileType.INSTANCE, """
|
|
class Unused {
|
|
private int ffff;
|
|
void foo(int p) {
|
|
if (p==0) return;
|
|
<caret>
|
|
}
|
|
}
|
|
""");
|
|
Document document = getDocument(getFile());
|
|
List<HighlightInfo> infos = doHighlighting(HighlightSeverity.WARNING);
|
|
assertSize(1, infos);
|
|
assertEquals("Private field 'ffff' is never used", infos.get(0).getDescription());
|
|
|
|
type(" foo(ffff++);");
|
|
assertEmpty(highlightErrors());
|
|
|
|
List<HighlightInfo> errors = DaemonCodeAnalyzerImpl.getHighlights(document, HighlightSeverity.WARNING, getProject());
|
|
assertSize(0, errors);
|
|
}
|
|
|
|
public void testUnusedMethodUpdate() {
|
|
configureByText(JavaFileType.INSTANCE, """
|
|
class X {
|
|
static void ffff() {}
|
|
public static void main(String[] args){
|
|
for (int i=0; i<1000;i++) {
|
|
System.out.println(i);
|
|
<caret>ffff();
|
|
}
|
|
}
|
|
}""");
|
|
enableInspectionTool(new UnusedDeclarationInspection(true));
|
|
List<HighlightInfo> infos = doHighlighting(HighlightSeverity.WARNING);
|
|
assertEmpty(infos);
|
|
|
|
PlatformTestUtil.invokeNamedAction(IdeActions.ACTION_COMMENT_LINE);
|
|
infos = doHighlighting(HighlightSeverity.WARNING);
|
|
|
|
assertSize(1, infos);
|
|
assertEquals("Method 'ffff()' is never used", infos.get(0).getDescription());
|
|
}
|
|
|
|
|
|
public void testAssignedButUnreadFieldUpdate() throws Exception {
|
|
configureByFile(BASE_PATH + "AssignedButUnreadField.java");
|
|
List<HighlightInfo> infos = doHighlighting(HighlightSeverity.WARNING);
|
|
assertSize(1, infos);
|
|
assertEquals("Private field 'text' is assigned but never accessed", infos.get(0).getDescription());
|
|
|
|
ctrlW();
|
|
WriteCommandAction.runWriteCommandAction(getProject(), () -> EditorModificationUtilEx.deleteSelectedText(getEditor()));
|
|
type(" text");
|
|
|
|
List<HighlightInfo> errors = doHighlighting(HighlightSeverity.WARNING);
|
|
assertEmpty(getFile().getText(), errors);
|
|
}
|
|
|
|
public void testDaemonIgnoresNonPhysicalEditor() throws Exception {
|
|
configureByText(JavaFileType.INSTANCE, """
|
|
class AClass<caret> {
|
|
|
|
}
|
|
""");
|
|
assertEmpty(highlightErrors());
|
|
|
|
EditorFactory editorFactory = EditorFactory.getInstance();
|
|
Document consoleDoc = editorFactory.createDocument("my console blah");
|
|
Editor consoleEditor = editorFactory.createEditor(consoleDoc);
|
|
|
|
try {
|
|
checkDaemonReaction(false, () -> caretRight(consoleEditor));
|
|
checkDaemonReaction(true, () -> typeInAlienEditor(consoleEditor, 'x'));
|
|
checkDaemonReaction(true, () -> LightPlatformCodeInsightTestCase.backspace(consoleEditor, getProject()));
|
|
|
|
//real editor
|
|
checkDaemonReaction(true, this::caretRight);
|
|
}
|
|
finally {
|
|
editorFactory.releaseEditor(consoleEditor);
|
|
}
|
|
}
|
|
|
|
|
|
public void testDaemonIgnoresConsoleActivities() throws Exception {
|
|
configureByText(JavaFileType.INSTANCE, """
|
|
class AClass<caret> {
|
|
|
|
}
|
|
""");
|
|
|
|
assertEmpty(highlightErrors());
|
|
|
|
ConsoleView consoleView = TextConsoleBuilderFactory.getInstance().createBuilder(getProject()).getConsole();
|
|
|
|
consoleView.getComponent(); //create editor
|
|
consoleView.print("haha", ConsoleViewContentType.NORMAL_OUTPUT);
|
|
UIUtil.dispatchAllInvocationEvents();
|
|
|
|
try {
|
|
checkDaemonReaction(false, () -> {
|
|
consoleView.clear();
|
|
try {
|
|
Thread.sleep(300); // *&^ing alarm
|
|
}
|
|
catch (InterruptedException e) {
|
|
LOG.error(e);
|
|
}
|
|
UIUtil.dispatchAllInvocationEvents(); //flush
|
|
});
|
|
checkDaemonReaction(false, () -> {
|
|
consoleView.print("sss", ConsoleViewContentType.NORMAL_OUTPUT);
|
|
try {
|
|
Thread.sleep(300); // *&^ing alarm
|
|
}
|
|
catch (InterruptedException e) {
|
|
LOG.error(e);
|
|
}
|
|
UIUtil.dispatchAllInvocationEvents(); //flush
|
|
});
|
|
checkDaemonReaction(false, () -> {
|
|
consoleView.setOutputPaused(true);
|
|
try {
|
|
Thread.sleep(300); // *&^ing alarm
|
|
}
|
|
catch (InterruptedException e) {
|
|
LOG.error(e);
|
|
}
|
|
UIUtil.dispatchAllInvocationEvents(); //flush
|
|
});
|
|
}
|
|
finally {
|
|
Disposer.dispose(consoleView);
|
|
}
|
|
}
|
|
|
|
private void checkDaemonReaction(boolean mustCancelItself, @NotNull Runnable action) throws Exception {
|
|
PsiDocumentManager.getInstance(getProject()).commitAllDocuments();
|
|
highlightErrors();
|
|
myDaemonCodeAnalyzer.waitForTermination();
|
|
TextEditor textEditor = TextEditorProvider.getInstance().getTextEditor(getEditor());
|
|
|
|
AtomicBoolean run = new AtomicBoolean();
|
|
Disposable disposable = Disposer.newDisposable();
|
|
AtomicReference<RuntimeException> stopDaemonReason = new AtomicReference<>();
|
|
StorageUtilKt.setDEBUG_LOG("");
|
|
getProject().getMessageBus().connect(disposable).subscribe(DaemonCodeAnalyzer.DAEMON_EVENT_TOPIC,
|
|
new DaemonCodeAnalyzer.DaemonListener() {
|
|
@Override
|
|
public void daemonCancelEventOccurred(@NotNull String reason) {
|
|
RuntimeException e = new RuntimeException("Some bastard's restarted daemon: " + reason +
|
|
"\nStorage write log: ----------\n" +
|
|
StorageUtilKt.getDEBUG_LOG() + "\n--------------");
|
|
stopDaemonReason.compareAndSet(null, e);
|
|
}
|
|
});
|
|
try {
|
|
while (true) {
|
|
Thread.onSpinWait();
|
|
try {
|
|
int[] toIgnore = new int[0];
|
|
Runnable callbackWhileWaiting = () -> {
|
|
if (!run.getAndSet(true)) {
|
|
action.run();
|
|
}
|
|
};
|
|
myDaemonCodeAnalyzer.runPasses(getFile(), getDocument(getFile()), textEditor, toIgnore, true, callbackWhileWaiting);
|
|
break;
|
|
}
|
|
catch (ProcessCanceledException ignored) { }
|
|
}
|
|
|
|
if (mustCancelItself) {
|
|
assertNotNull(stopDaemonReason.get());
|
|
}
|
|
else {
|
|
if (stopDaemonReason.get() != null) throw stopDaemonReason.get();
|
|
}
|
|
}
|
|
finally {
|
|
StorageUtilKt.setDEBUG_LOG(null);
|
|
Disposer.dispose(disposable);
|
|
}
|
|
}
|
|
|
|
public void testChangeXmlIncludeLeadsToRehighlight() {
|
|
LanguageFilter[] extensions = XMLLanguage.INSTANCE.getLanguageExtensions();
|
|
for (LanguageFilter extension : extensions) {
|
|
XMLLanguage.INSTANCE.unregisterLanguageExtension(extension);
|
|
}
|
|
|
|
String location = getTestName(false) + ".xsd";
|
|
final String url = "http://myschema/";
|
|
ExternalResourceManagerExImpl.registerResourceTemporarily(url, location, getTestRootDisposable());
|
|
|
|
configureByFiles(null, BASE_PATH + getTestName(false) + ".xml", BASE_PATH + getTestName(false) + ".xsd");
|
|
|
|
assertEmpty(highlightErrors());
|
|
|
|
Editor[] allEditors = EditorFactory.getInstance().getAllEditors();
|
|
Editor schemaEditor = null;
|
|
for (Editor editor : allEditors) {
|
|
Document document = editor.getDocument();
|
|
PsiFile psiFile = PsiDocumentManager.getInstance(getProject()).getPsiFile(document);
|
|
if (psiFile == null) continue;
|
|
if (location.equals(psiFile.getName())) {
|
|
schemaEditor = editor;
|
|
break;
|
|
}
|
|
}
|
|
delete(Objects.requireNonNull(schemaEditor));
|
|
|
|
assertNotEmpty(highlightErrors());
|
|
|
|
for (LanguageFilter extension : extensions) {
|
|
XMLLanguage.INSTANCE.registerLanguageExtension(extension);
|
|
}
|
|
}
|
|
|
|
|
|
public void testRehighlightInnerBlockAfterInline() throws Exception {
|
|
configureByFile(BASE_PATH + getTestName(false) + ".java");
|
|
|
|
HighlightInfo error = assertOneElement(highlightErrors());
|
|
assertEquals("Variable 'e' is already defined in the scope", error.getDescription());
|
|
PsiElement element = getFile().findElementAt(getEditor().getCaretModel().getOffset()).getParent();
|
|
|
|
DataContext dataContext = SimpleDataContext.getSimpleContext(CommonDataKeys.PSI_ELEMENT, element, ((EditorEx)getEditor()).getDataContext());
|
|
new InlineRefactoringActionHandler().invoke(getProject(), getEditor(), getFile(), dataContext);
|
|
|
|
assertEmpty(highlightErrors());
|
|
}
|
|
|
|
|
|
public void testRangeMarkersDoNotGetAddedOrRemovedWhenUserIsJustTypingInsideHighlightedRegionAndEspeciallyInsideInjectedFragmentsWhichAreColoredGreenAndUsersComplainEndlesslyThatEditorFlickersThere() {
|
|
configureByText(JavaFileType.INSTANCE, """
|
|
class S { int f() {
|
|
return <caret>hashCode();
|
|
}}""");
|
|
|
|
Collection<HighlightInfo> infos = doHighlighting(HighlightInfoType.SYMBOL_TYPE_SEVERITY);
|
|
assertSize(3, infos);
|
|
|
|
AtomicInteger count = new AtomicInteger();
|
|
MarkupModelEx modelEx = (MarkupModelEx)DocumentMarkupModel.forDocument(getDocument(getFile()), getProject(), true);
|
|
modelEx.addMarkupModelListener(getTestRootDisposable(), new MarkupModelListener() {
|
|
@Override
|
|
public void afterAdded(@NotNull RangeHighlighterEx highlighter) {
|
|
count.incrementAndGet();
|
|
}
|
|
|
|
@Override
|
|
public void afterRemoved(@NotNull RangeHighlighterEx highlighter) {
|
|
count.incrementAndGet();
|
|
}
|
|
});
|
|
|
|
type(' ');
|
|
assertEmpty(highlightErrors());
|
|
|
|
assertEquals(0, count.get());
|
|
}
|
|
|
|
public void testTypeParametersMustNotBlinkWhenTypingInsideClass() {
|
|
configureByText(JavaFileType.INSTANCE, """
|
|
package x;
|
|
abstract class ToRun<TTTTTTTTTTTTTTT> implements Comparable<TTTTTTTTTTTTTTT> {
|
|
private ToRun<TTTTTTTTTTTTTTT> delegate;
|
|
<caret>
|
|
\s
|
|
}""");
|
|
|
|
assertEmpty(highlightErrors());
|
|
|
|
MarkupModelEx modelEx = (MarkupModelEx)DocumentMarkupModel.forDocument(getDocument(getFile()), getProject(), true);
|
|
modelEx.addMarkupModelListener(getTestRootDisposable(), new MarkupModelListener() {
|
|
@Override
|
|
public void beforeRemoved(@NotNull RangeHighlighterEx highlighter) {
|
|
if (highlighter.getTextRange().substring(highlighter.getDocument().getText()).equals("TTTTTTTTTTTTTTT")) {
|
|
throw new RuntimeException("Must not remove type parameter highlighter");
|
|
}
|
|
}
|
|
});
|
|
|
|
assertEmpty(highlightErrors());
|
|
|
|
type("//xxx");
|
|
assertEmpty(highlightErrors());
|
|
backspace();
|
|
assertEmpty(highlightErrors());
|
|
backspace();
|
|
assertEmpty(highlightErrors());
|
|
backspace();
|
|
assertEmpty(highlightErrors());
|
|
backspace();
|
|
backspace();
|
|
assertEmpty(highlightErrors());
|
|
}
|
|
|
|
public void testLocallyUsedFieldHighlighting() {
|
|
configureByText(JavaFileType.INSTANCE, """
|
|
class A {
|
|
String cons;
|
|
void foo() {
|
|
String local = null;
|
|
<selection>cons</selection>.substring(1); }
|
|
public static void main(String[] args) {
|
|
new A().foo();
|
|
}}""");
|
|
enableInspectionTool(new UnusedDeclarationInspection(true));
|
|
|
|
List<HighlightInfo> infos = doHighlighting(HighlightSeverity.WARNING);
|
|
assertSize(1, infos);
|
|
assertEquals("Variable 'local' is never used", infos.get(0).getDescription());
|
|
|
|
type("local");
|
|
|
|
infos = doHighlighting(HighlightSeverity.WARNING);
|
|
assertSize(1, infos);
|
|
assertEquals("Field 'cons' is never used", infos.get(0).getDescription());
|
|
}
|
|
|
|
public void testOverrideMethodsHighlightingPersistWhenTypeInsideMethodBody() {
|
|
@Language("JAVA")
|
|
String text = """
|
|
package x;\s
|
|
class ClassA {
|
|
static <T> void sayHello(Class<? extends T> msg) {}
|
|
}
|
|
class ClassB extends ClassA {
|
|
static <T extends String> void sayHello(Class<? extends T> msg) {<caret>
|
|
}
|
|
}
|
|
""";
|
|
configureByText(JavaFileType.INSTANCE, text);
|
|
|
|
assertOneElement(highlightErrors());
|
|
type("//my comment inside method body, so class modifier won't be visited");
|
|
assertOneElement(highlightErrors());
|
|
}
|
|
|
|
public void testWhenTypingOverWrongReferenceIncludingRightAfterTheEndAndRightBeforeStartItsColorMustStayTheRedWithoutAnyBlinking() {
|
|
configureByText(JavaFileType.INSTANCE, """
|
|
class S { int f() {
|
|
return asfsdfsdfsd<caret>;
|
|
}}""");
|
|
|
|
HighlightInfo error = assertOneElement(highlightErrors());
|
|
assertSame(HighlightInfoType.WRONG_REF, error.type);
|
|
|
|
Document document = getDocument(getFile());
|
|
|
|
type("xxx");
|
|
|
|
// right after typing, before the highlighting kicked in, its color must stay red
|
|
for (HighlightInfo info : DaemonCodeAnalyzerImpl.getHighlights(document, HighlightInfoType.SYMBOL_TYPE_SEVERITY, getProject())) {
|
|
if (TextRange.create(info).intersects(error)) {
|
|
assertSame(HighlightInfoType.WRONG_REF, info.type);
|
|
assertEquals("asfsdfsdfsd" + "xxx", info.getText());
|
|
}
|
|
}
|
|
|
|
getEditor().getCaretModel().moveToOffset(error.startOffset);
|
|
type("zzz");
|
|
for (HighlightInfo info : DaemonCodeAnalyzerImpl.getHighlights(document, HighlightInfoType.SYMBOL_TYPE_SEVERITY, getProject())) {
|
|
if (TextRange.create(info).intersects(error)) {
|
|
assertSame(HighlightInfoType.WRONG_REF, info.type);
|
|
assertEquals("zzz" + "asfsdfsdfsd" + "xxx", info.getText());
|
|
}
|
|
}
|
|
|
|
HighlightInfo error2 = assertOneElement(highlightErrors());
|
|
assertSame(HighlightInfoType.WRONG_REF, error2.type);
|
|
}
|
|
|
|
|
|
public void testQuickFixRemainsAvailableAfterAnotherFixHasBeenAppliedInTheSameCodeBlockBefore() throws Exception {
|
|
configureByFile(BASE_PATH + "QuickFixes.java");
|
|
|
|
DaemonCodeAnalyzerSettings settings = DaemonCodeAnalyzerSettings.getInstance();
|
|
boolean old = settings.isNextErrorActionGoesToErrorsFirst();
|
|
settings.setNextErrorActionGoesToErrorsFirst(true);
|
|
|
|
try {
|
|
Collection<HighlightInfo> errors = highlightErrors();
|
|
assertSize(3, errors);
|
|
new GotoNextErrorHandler(true).invoke(getProject(), getEditor(), getFile());
|
|
|
|
List<IntentionAction> fixes = LightQuickFixTestCase.getAvailableActions(getEditor(), getFile());
|
|
IntentionAction fix = assertContainsOneOf(fixes, "Delete catch for 'java.io.IOException'");
|
|
|
|
IntentionAction finalFix = fix;
|
|
WriteCommandAction.runWriteCommandAction(getProject(), () -> finalFix.invoke(getProject(), getEditor(), getFile()));
|
|
|
|
errors = highlightErrors();
|
|
assertSize(2, errors);
|
|
|
|
new GotoNextErrorHandler(true).invoke(getProject(), getEditor(), getFile());
|
|
fixes = LightQuickFixTestCase.getAvailableActions(getEditor(), getFile());
|
|
fix = assertContainsOneOf(fixes, "Delete catch for 'java.io.IOException'");
|
|
|
|
IntentionAction finalFix1 = fix;
|
|
WriteCommandAction.runWriteCommandAction(getProject(), () -> finalFix1.invoke(getProject(), getEditor(), getFile()));
|
|
|
|
assertOneElement(highlightErrors());
|
|
|
|
new GotoNextErrorHandler(true).invoke(getProject(), getEditor(), getFile());
|
|
fixes = LightQuickFixTestCase.getAvailableActions(getEditor(), getFile());
|
|
fix = assertContainsOneOf(fixes, "Delete catch for 'java.io.IOException'");
|
|
|
|
IntentionAction finalFix2 = fix;
|
|
WriteCommandAction.runWriteCommandAction(getProject(), () -> finalFix2.invoke(getProject(), getEditor(), getFile()));
|
|
|
|
assertEmpty(highlightErrors());
|
|
}
|
|
finally {
|
|
settings.setNextErrorActionGoesToErrorsFirst(old);
|
|
}
|
|
}
|
|
|
|
private static IntentionAction assertContainsOneOf(@NotNull Collection<? extends IntentionAction> collection, @NotNull String text) {
|
|
IntentionAction result = null;
|
|
for (IntentionAction action : collection) {
|
|
if (text.equals(action.getText())) {
|
|
if (result != null) {
|
|
fail("multiple " + " objects present in collection " + collection);
|
|
}
|
|
else {
|
|
result = action;
|
|
}
|
|
}
|
|
}
|
|
assertNotNull(" object not found in collection " + collection, result);
|
|
return result;
|
|
}
|
|
|
|
|
|
public void testRangeHighlightersDoNotGetStuckForever() {
|
|
configureByText(JavaFileType.INSTANCE, "class S { void ffffff() {fff<caret>fff();}}");
|
|
|
|
assertEmpty(highlightErrors());
|
|
MarkupModel markup = DocumentMarkupModel.forDocument(myEditor.getDocument(), getProject(), true);
|
|
TextRange[] highlightersBefore = getHighlightersTextRange(markup);
|
|
|
|
type("%%%%");
|
|
assertNotEmpty(highlightErrors());
|
|
backspace();
|
|
backspace();
|
|
backspace();
|
|
backspace();
|
|
assertEmpty(highlightErrors());
|
|
|
|
TextRange[] highlightersAfter = getHighlightersTextRange(markup);
|
|
|
|
assertSize(highlightersBefore.length, highlightersAfter);
|
|
for (int i = 0; i < highlightersBefore.length; i++) {
|
|
TextRange before = highlightersBefore[i];
|
|
TextRange after = highlightersAfter[i];
|
|
assertEquals(before.getStartOffset(), after.getStartOffset());
|
|
assertEquals(before.getEndOffset(), after.getEndOffset());
|
|
}
|
|
}
|
|
|
|
private static TextRange @NotNull [] getHighlightersTextRange(@NotNull MarkupModel markup) {
|
|
RangeHighlighter[] highlighters = markup.getAllHighlighters();
|
|
|
|
TextRange[] result = new TextRange[highlighters.length];
|
|
for (int i = 0; i < highlighters.length; i++) {
|
|
result[i] = ProperTextRange.create(highlighters[i]);
|
|
}
|
|
return orderByHashCode(result); // markup.getAllHighlighters returns unordered array
|
|
}
|
|
|
|
private static <T extends Segment> T @NotNull [] orderByHashCode(T @NotNull [] highlighters) {
|
|
Arrays.sort(highlighters, (o1, o2) -> o2.hashCode() - o1.hashCode());
|
|
return highlighters;
|
|
}
|
|
|
|
public void testModificationInsideCodeBlockDoesNotAffectErrorMarkersOutside() {
|
|
configureByText(JavaFileType.INSTANCE, """
|
|
class SSSSS {
|
|
public static void suite() {
|
|
<caret>
|
|
new Runnable() {
|
|
public void run() {
|
|
|
|
}
|
|
};
|
|
}
|
|
|
|
""");
|
|
HighlightInfo error = assertOneElement(highlightErrors());
|
|
assertEquals("'}' expected", error.getDescription());
|
|
|
|
type("//comment");
|
|
HighlightInfo error2 = assertOneElement(highlightErrors());
|
|
assertEquals("'}' expected", error2.getDescription());
|
|
}
|
|
|
|
public void testErrorMarkerAtTheEndOfTheFile() {
|
|
CommandProcessor.getInstance().executeCommand(getProject(), () -> {
|
|
try {
|
|
configureByFile(BASE_PATH + "ErrorMarkAtEnd.java");
|
|
}
|
|
catch (Exception e) {
|
|
LOG.error(e);
|
|
}
|
|
}, "Cc", this);
|
|
EditorTestUtil.setEditorVisibleSizeInPixels(getEditor(), 1000, 1000);
|
|
assertEmpty(highlightErrors());
|
|
CommandProcessor.getInstance().executeCommand(getProject(), () -> {
|
|
Document document = getEditor().getDocument();
|
|
int offset = getEditor().getCaretModel().getOffset();
|
|
while (offset < document.getTextLength()) {
|
|
int i = StringUtil.indexOf(document.getText(), '}', offset, document.getTextLength());
|
|
if (i == -1) break;
|
|
getEditor().getCaretModel().moveToOffset(i);
|
|
delete(getEditor());
|
|
}
|
|
}, "My", this);
|
|
|
|
List<HighlightInfo> errs = highlightErrors();
|
|
assertSize(2, errs);
|
|
assertEquals("'}' expected", errs.get(0).getDescription());
|
|
|
|
undo();
|
|
assertEmpty(highlightErrors());
|
|
}
|
|
|
|
// todo - StoreUtil.saveDocumentsAndProjectsAndApp cannot save in EDT. If it is called in EDT,
|
|
// in this case, task is done under a modal progress, so, no idea how to fix the test, except executing it not in EDT (as it should be)
|
|
public void _testDaemonIgnoresFrameDeactivation() {
|
|
// return default value to avoid unnecessary save
|
|
DaemonCodeAnalyzerSettings.getInstance().setImportHintEnabled(true);
|
|
|
|
String text = "class S { ArrayList<caret>XXX x;}";
|
|
configureByText(JavaFileType.INSTANCE, text);
|
|
assertNotEmpty(highlightErrors());
|
|
|
|
GeneralSettings settings = GeneralSettings.getInstance();
|
|
boolean frameSave = settings.isSaveOnFrameDeactivation();
|
|
settings.setSaveOnFrameDeactivation(true);
|
|
StoreUtilKt.runInAllowSaveMode(true, () -> {
|
|
try {
|
|
StoreUtil.saveDocumentsAndProjectsAndApp(false);
|
|
|
|
try {
|
|
checkDaemonReaction(false, () -> StoreUtil.saveDocumentsAndProjectsAndApp(false));
|
|
}
|
|
catch (Exception e) {
|
|
throw new RuntimeException(e);
|
|
}
|
|
}
|
|
finally {
|
|
settings.setSaveOnFrameDeactivation(frameSave);
|
|
}
|
|
return Unit.INSTANCE;
|
|
});
|
|
}
|
|
|
|
public void testApplyLocalQuickFix() {
|
|
configureByText(JavaFileType.INSTANCE, "class X { static int sss; public int f() { return this.<caret>sss; }}");
|
|
|
|
((EditorImpl)myEditor).getScrollPane().getViewport().setSize(1000, 1000);
|
|
DaemonCodeAnalyzerSettings.getInstance().setImportHintEnabled(true);
|
|
|
|
List<HighlightInfo> warns = doHighlighting(HighlightSeverity.WARNING);
|
|
assertOneElement(warns);
|
|
Editor editor = getEditor();
|
|
List<HighlightInfo.IntentionActionDescriptor> actions =
|
|
ShowIntentionsPass.getAvailableFixes(editor, getFile(), -1, ((EditorEx)editor).getExpectedCaretOffset());
|
|
HighlightInfo.IntentionActionDescriptor descriptor = assertOneElement(actions);
|
|
CodeInsightTestFixtureImpl.invokeIntention(descriptor.getAction(), getFile(), getEditor());
|
|
|
|
assertEmpty(highlightErrors());
|
|
assertEmpty(ShowIntentionsPass.getAvailableFixes(editor, getFile(), -1, ((EditorEx)editor).getExpectedCaretOffset()));
|
|
}
|
|
|
|
|
|
public void testApplyErrorInTheMiddle() {
|
|
String text = "class <caret>X { " + """
|
|
|
|
{
|
|
// String x = "<zzzzzzzzzz/>";
|
|
}""".repeat(100) +
|
|
"\n}";
|
|
configureByText(JavaFileType.INSTANCE, text);
|
|
|
|
((EditorImpl)myEditor).getScrollPane().getViewport().setSize(1000, 1000);
|
|
DaemonCodeAnalyzerSettings.getInstance().setImportHintEnabled(true);
|
|
|
|
assertEmpty(highlightErrors());
|
|
|
|
type("//");
|
|
List<HighlightInfo> errors = highlightErrors();
|
|
assertSize(2, errors);
|
|
|
|
backspace();
|
|
backspace();
|
|
|
|
assertEmpty(highlightErrors());
|
|
}
|
|
|
|
|
|
public void testErrorInTheEndOutsideVisibleArea() {
|
|
String text = "<xml> \n" + StringUtil.repeatSymbol('\n', 1000) + "</xml>\nxxxxx<caret>";
|
|
configureByText(XmlFileType.INSTANCE, text);
|
|
|
|
ProperTextRange visibleRange = makeEditorWindowVisible(new Point(0, 1000), myEditor);
|
|
assertTrue(visibleRange.getStartOffset() > 0);
|
|
|
|
HighlightInfo info = assertOneElement(highlightErrors());
|
|
assertEquals("Top level element is not completed", info.getDescription());
|
|
|
|
type("xxx");
|
|
info = assertOneElement(highlightErrors());
|
|
assertEquals("Top level element is not completed", info.getDescription());
|
|
}
|
|
|
|
@NotNull
|
|
public static ProperTextRange makeEditorWindowVisible(@NotNull Point viewPosition, @NotNull Editor editor) {
|
|
DaemonCodeAnalyzerSettings.getInstance().setImportHintEnabled(true);
|
|
int offset = editor.logicalPositionToOffset(editor.xyToLogicalPosition(viewPosition));
|
|
VisibleHighlightingPassFactory.setVisibleRangeForHeadlessMode(editor, new ProperTextRange(offset, offset));
|
|
return editor.calculateVisibleRange();
|
|
}
|
|
|
|
static void makeWholeEditorWindowVisible(@NotNull EditorImpl editor) {
|
|
DaemonCodeAnalyzerSettings.getInstance().setImportHintEnabled(true);
|
|
VisibleHighlightingPassFactory.setVisibleRangeForHeadlessMode(editor, new ProperTextRange(0, editor.getDocument().getTextLength()));
|
|
}
|
|
|
|
|
|
public void testEnterInCodeBlock() {
|
|
String text = """
|
|
class LQF {
|
|
int wwwwwwwwwwww;
|
|
public void main() {<caret>
|
|
wwwwwwwwwwww = 1;
|
|
}
|
|
}""";
|
|
configureByText(JavaFileType.INSTANCE, text);
|
|
|
|
((EditorImpl)myEditor).getScrollPane().getViewport().setSize(1000, 1000);
|
|
|
|
List<HighlightInfo> infos = doHighlighting(HighlightInfoType.SYMBOL_TYPE_SEVERITY);
|
|
assertSize(4, infos);
|
|
|
|
type('\n');
|
|
infos = doHighlighting(HighlightInfoType.SYMBOL_TYPE_SEVERITY);
|
|
assertSize(4, infos);
|
|
|
|
deleteLine();
|
|
|
|
infos = doHighlighting(HighlightInfoType.SYMBOL_TYPE_SEVERITY);
|
|
assertSize(4, infos);
|
|
}
|
|
|
|
|
|
public void testTypingNearEmptyErrorElement() {
|
|
String text = """
|
|
class LQF {
|
|
public void main() {
|
|
int wwwwwwwwwwww = 1<caret>
|
|
}
|
|
}""";
|
|
configureByText(JavaFileType.INSTANCE, text);
|
|
|
|
((EditorImpl)myEditor).getScrollPane().getViewport().setSize(1000, 1000);
|
|
|
|
assertOneElement(highlightErrors());
|
|
|
|
type(';');
|
|
assertEmpty(highlightErrors());
|
|
}
|
|
|
|
|
|
|
|
public void testCancelsItSelfOnTypingInAlienProject() throws Throwable {
|
|
String body = StringUtil.repeat("\"String field = null;\"\n", 1000);
|
|
configureByText(JavaFileType.INSTANCE, "class X{ void f() {" + body + "<caret>\n} }");
|
|
|
|
Project alienProject = PlatformTestUtil.loadAndOpenProject(createTempDirectory().toPath().resolve("alien.ipr"), getTestRootDisposable());
|
|
DaemonProgressIndicator.runInDebugMode(() -> {
|
|
try {
|
|
Module alienModule = doCreateRealModuleIn("x", alienProject, getModuleType());
|
|
VirtualFile alienRoot = createTestProjectStructure(alienModule, null, true, getTempDir());
|
|
PsiDocumentManager.getInstance(alienProject).commitAllDocuments();
|
|
OpenFileDescriptor alienDescriptor = WriteAction.compute(() -> {
|
|
VirtualFile alienFile = alienRoot.createChildData(this, "AlienFile.java");
|
|
setFileText(alienFile, "class Alien { }");
|
|
return new OpenFileDescriptor(alienProject, alienFile);
|
|
});
|
|
|
|
FileEditorManager fe = FileEditorManager.getInstance(alienProject);
|
|
Editor alienEditor = Objects.requireNonNull(fe.openTextEditor(alienDescriptor, false));
|
|
((EditorImpl)alienEditor).setCaretActive();
|
|
PsiDocumentManager.getInstance(getProject()).commitAllDocuments();
|
|
PsiDocumentManager.getInstance(alienProject).commitAllDocuments();
|
|
|
|
// start daemon in the main project. should check for its cancel when typing in alien
|
|
TextEditor textEditor = TextEditorProvider.getInstance().getTextEditor(getEditor());
|
|
AtomicBoolean checked = new AtomicBoolean();
|
|
Runnable callbackWhileWaiting = () -> {
|
|
if (checked.getAndSet(true)) return;
|
|
typeInAlienEditor(alienEditor, 'x');
|
|
};
|
|
myDaemonCodeAnalyzer.runPasses(getFile(), getEditor().getDocument(), textEditor, ArrayUtilRt.EMPTY_INT_ARRAY, true, callbackWhileWaiting);
|
|
}
|
|
catch (ProcessCanceledException ignored) {
|
|
return;
|
|
}
|
|
fail("must throw PCE");
|
|
});
|
|
}
|
|
|
|
public void testPasteInAnonymousCodeBlock() {
|
|
@Language("JAVA")
|
|
String text = """
|
|
class X{ void f() { int x=0;x++;
|
|
Runnable r = new Runnable() { public void run() {
|
|
<caret>
|
|
}};
|
|
<selection>int y = x;</selection>
|
|
|
|
} }""";
|
|
configureByText(JavaFileType.INSTANCE, text);
|
|
assertEmpty(highlightErrors());
|
|
PlatformTestUtil.invokeNamedAction(IdeActions.ACTION_EDITOR_COPY);
|
|
assertEquals("int y = x;", getEditor().getSelectionModel().getSelectedText());
|
|
getEditor().getSelectionModel().removeSelection();
|
|
PlatformTestUtil.invokeNamedAction(IdeActions.ACTION_EDITOR_PASTE);
|
|
assertOneElement(highlightErrors());
|
|
}
|
|
|
|
public void testPostHighlightingPassRunsOnEveryPsiModification() throws Exception {
|
|
@Language("JAVA")
|
|
String xText = "public class X { public static void ffffffffffffff(){} }";
|
|
PsiFile x = createFile("X.java", xText);
|
|
PsiFile use = createFile("Use.java", "public class Use { { <caret>X.ffffffffffffff(); } }");
|
|
configureByExistingFile(use.getVirtualFile());
|
|
|
|
enableDeadCodeInspection();
|
|
|
|
Editor xEditor = createEditor(x.getVirtualFile());
|
|
List<HighlightInfo> xInfos = filter(CodeInsightTestFixtureImpl.instantiateAndRun(x, xEditor, new int[0], false),
|
|
HighlightSeverity.WARNING);
|
|
HighlightInfo info = ContainerUtil.find(xInfos, xInfo -> xInfo.getDescription().equals("Method 'ffffffffffffff()' is never used"));
|
|
assertNull(xInfos.toString(), info);
|
|
|
|
Editor useEditor = myEditor;
|
|
List<HighlightInfo> useInfos = filter(CodeInsightTestFixtureImpl.instantiateAndRun(use, useEditor, new int[0], false), HighlightSeverity.ERROR);
|
|
assertEmpty(useInfos);
|
|
|
|
type('/');
|
|
type('/');
|
|
|
|
PsiDocumentManager.getInstance(getProject()).commitAllDocuments();
|
|
xInfos = filter(CodeInsightTestFixtureImpl.instantiateAndRun(x, xEditor, new int[0], false), HighlightSeverity.WARNING);
|
|
info = ContainerUtil.find(xInfos, xInfo -> xInfo.getDescription().equals("Method 'ffffffffffffff()' is never used"));
|
|
assertNotNull(xInfos.toString(), info);
|
|
}
|
|
|
|
private void enableDeadCodeInspection() {
|
|
InspectionProfile profile = InspectionProjectProfileManager.getInstance(myProject).getCurrentProfile();
|
|
HighlightDisplayKey myDeadCodeKey = HighlightDisplayKey.findOrRegister(UnusedDeclarationInspectionBase.SHORT_NAME,
|
|
UnusedDeclarationInspectionBase.getDisplayNameText(), UnusedDeclarationInspectionBase.SHORT_NAME);
|
|
UnusedDeclarationInspectionBase myDeadCodeInspection = new UnusedDeclarationInspectionBase(true);
|
|
enableInspectionTool(myDeadCodeInspection);
|
|
assert profile.isToolEnabled(myDeadCodeKey, myFile);
|
|
}
|
|
|
|
public void testErrorDisappearsRightAfterTypingInsideVisibleAreaWhileDaemonContinuesToChugAlong_Stress() {
|
|
String text = "class X{\nint xxx;\n{\nint i = <selection>null</selection><caret>;\n" + StringUtil.repeat("{ this.hashCode(); }\n\n\n", 10000) + "}}";
|
|
configureByText(JavaFileType.INSTANCE, text);
|
|
|
|
((EditorImpl)myEditor).getScrollPane().getViewport().setSize(100, 100);
|
|
DaemonCodeAnalyzerSettings.getInstance().setImportHintEnabled(true);
|
|
|
|
myEditor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE);
|
|
((EditorImpl)myEditor).getScrollPane().getViewport().setViewPosition(new Point(0, 0));
|
|
((EditorImpl)myEditor).getScrollPane().getViewport().setExtentSize(new Dimension(100, 100000));
|
|
@NotNull Editor editor = getEditor();
|
|
ProperTextRange visibleRange = editor.calculateVisibleRange();
|
|
assertTrue(visibleRange.getLength() > 0);
|
|
Document document = myEditor.getDocument();
|
|
assertTrue(visibleRange.getLength() < document.getTextLength());
|
|
|
|
HighlightInfo info = assertOneElement(highlightErrors());
|
|
final String errorDescription = "Incompatible types. Found: 'null', required: 'int'";
|
|
assertEquals(errorDescription, info.getDescription());
|
|
|
|
MarkupModelEx model = (MarkupModelEx)DocumentMarkupModel.forDocument(document, myProject, false);
|
|
AtomicBoolean errorRemoved = new AtomicBoolean();
|
|
|
|
model.addMarkupModelListener(getTestRootDisposable(), new MarkupModelListener() {
|
|
@Override
|
|
public void afterRemoved(@NotNull RangeHighlighterEx highlighter) {
|
|
HighlightInfo info = HighlightInfo.fromRangeHighlighter(highlighter);
|
|
if (info == null) return;
|
|
String description = info.getDescription();
|
|
if (errorDescription.equals(description)) {
|
|
errorRemoved.set(true);
|
|
|
|
List<ProgressableTextEditorHighlightingPass> passes = myDaemonCodeAnalyzer.getPassesToShowProgressFor(document);
|
|
GeneralHighlightingPass ghp = null;
|
|
for (TextEditorHighlightingPass pass : passes) {
|
|
if (pass instanceof GeneralHighlightingPass && pass.getId() == Pass.UPDATE_ALL) {
|
|
assert ghp == null : ghp;
|
|
ghp = (GeneralHighlightingPass)pass;
|
|
}
|
|
}
|
|
assertNotNull(ghp);
|
|
boolean finished = ghp.isFinished();
|
|
assertFalse(finished);
|
|
}
|
|
}
|
|
});
|
|
type("1");
|
|
|
|
assertEmpty(highlightErrors());
|
|
assertTrue(errorRemoved.get());
|
|
}
|
|
|
|
public void testDaemonWorksForDefaultProjectSinceItIsNeededInSettingsDialogForSomeReason() {
|
|
assertNotNull(DaemonCodeAnalyzer.getInstance(ProjectManager.getInstance().getDefaultProject()));
|
|
}
|
|
|
|
public void testChangeEventsAreNotAlwaysGeneric() {
|
|
String body = """
|
|
class X {
|
|
<caret> @org.PPP
|
|
void dtoArrayDouble() {
|
|
}
|
|
}""";
|
|
configureByText(JavaFileType.INSTANCE, body);
|
|
makeEditorWindowVisible(new Point(), myEditor);
|
|
|
|
assertNotEmpty(highlightErrors());
|
|
|
|
type("//");
|
|
assertEmpty(highlightErrors());
|
|
|
|
backspace();
|
|
backspace();
|
|
assertNotEmpty(highlightErrors());
|
|
}
|
|
|
|
public void testInterruptOnTyping_Stress() throws Throwable {
|
|
@NonNls String filePath = "/psi/resolve/Thinlet.java";
|
|
configureByFile(filePath);
|
|
assertEmpty(highlightErrors());
|
|
|
|
myDaemonCodeAnalyzer.restart(getTestName(false));
|
|
try {
|
|
PsiDocumentManager.getInstance(myProject).commitAllDocuments();
|
|
|
|
PsiFile psiFile = getFile();
|
|
Editor editor = getEditor();
|
|
Project project = psiFile.getProject();
|
|
CodeInsightTestFixtureImpl.ensureIndexesUpToDate(project);
|
|
TextEditor textEditor = TextEditorProvider.getInstance().getTextEditor(editor);
|
|
Runnable callbackWhileWaiting = () -> type(' ');
|
|
myDaemonCodeAnalyzer.runPasses(psiFile, editor.getDocument(), textEditor, ArrayUtilRt.EMPTY_INT_ARRAY, true, callbackWhileWaiting);
|
|
}
|
|
catch (ProcessCanceledException ignored) {
|
|
return;
|
|
}
|
|
fail("PCE must have been thrown");
|
|
}
|
|
|
|
public void testTypingInsideCodeBlockDoesntLeadToCatastrophicUnusedEverything_Stress() throws Throwable {
|
|
InspectionProfileImpl profile = InspectionProfileManager.getInstance(getProject()).getCurrentProfile();
|
|
profile.disableAllTools(getProject());
|
|
@NonNls String filePath = "/psi/resolve/Thinlet.java";
|
|
configureByFile(filePath);
|
|
|
|
PsiDocumentManager.getInstance(myProject).commitAllDocuments();
|
|
|
|
PsiFile psiFile = getFile();
|
|
Project project = psiFile.getProject();
|
|
CodeInsightTestFixtureImpl.ensureIndexesUpToDate(project);
|
|
List<HighlightInfo> errors = doHighlighting(HighlightSeverity.ERROR);
|
|
assertEmpty(errors);
|
|
List<HighlightInfo> initialWarnings = doHighlighting(HighlightSeverity.WARNING);
|
|
assertEmpty(initialWarnings);
|
|
int N_BLOCKS = codeBlocks(psiFile).size();
|
|
assertTrue("codeblocks :"+N_BLOCKS, N_BLOCKS > 1000);
|
|
Random random = new Random();
|
|
int N = 10;
|
|
// try with both serialized and not-serialized passes
|
|
myDaemonCodeAnalyzer.serializeCodeInsightPasses(false);
|
|
for (int i=0; i<N*2; i++) {
|
|
PsiCodeBlock block = codeBlocks(psiFile).get(random.nextInt(N_BLOCKS));
|
|
getEditor().getCaretModel().moveToOffset(block.getLBrace().getTextOffset() + 1);
|
|
type("\n/*xxx*/");
|
|
List<HighlightInfo> warnings = doHighlighting(HighlightSeverity.WARNING);
|
|
if (!warnings.isEmpty()) {
|
|
System.out.println("\n-----\n"+getEditor().getDocument().getText()+"\n--------\n");
|
|
}
|
|
assertEmpty(warnings);
|
|
if (i == N) {
|
|
// repeat the same steps with serialized passes
|
|
myDaemonCodeAnalyzer.serializeCodeInsightPasses(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
@NotNull
|
|
private List<PsiCodeBlock> codeBlocks(@NotNull PsiFile psiFile) {
|
|
List<PsiCodeBlock> blocks = new ArrayList<>();
|
|
psiFile.accept(new JavaRecursiveElementWalkingVisitor() {
|
|
@Override
|
|
public void visitCodeBlock(@NotNull PsiCodeBlock block) {
|
|
blocks.add(block);
|
|
super.visitCodeBlock(block);
|
|
}
|
|
});
|
|
return blocks;
|
|
}
|
|
|
|
public void testCodeFoldingInSplittedWindowAppliesToAllEditors() throws Exception {
|
|
Set<Editor> applied = ConcurrentCollectionFactory.createConcurrentSet();
|
|
Set<Editor> collected = ConcurrentCollectionFactory.createConcurrentSet();
|
|
registerFakePass(applied, collected);
|
|
|
|
configureByText(PlainTextFileType.INSTANCE, "");
|
|
Editor editor1 = getEditor();
|
|
Editor editor2 = EditorFactory.getInstance().createEditor(editor1.getDocument(),getProject());
|
|
Disposer.register(getTestRootDisposable(), () -> EditorFactory.getInstance().releaseEditor(editor2));
|
|
TextEditor textEditor1 = new PsiAwareTextEditorProvider().getTextEditor(editor1);
|
|
TextEditor textEditor2 = new PsiAwareTextEditorProvider().getTextEditor(editor2);
|
|
|
|
myDaemonCodeAnalyzer.runPasses(myFile, editor1.getDocument(), textEditor1, new int[0], false, null);
|
|
myDaemonCodeAnalyzer.runPasses(myFile, editor1.getDocument(), textEditor2, new int[0], false, null);
|
|
List<HighlightInfo> errors = DaemonCodeAnalyzerImpl.getHighlights(editor1.getDocument(), null, myProject);
|
|
assertEmpty(errors);
|
|
|
|
assertEquals(collected, ContainerUtil.newHashSet(editor1, editor2));
|
|
assertEquals(applied, ContainerUtil.newHashSet(editor1, editor2));
|
|
}
|
|
|
|
// checks that only one instance is running
|
|
public static class MySingletonAnnotator extends DaemonAnnotatorsRespondToChangesTest.MyRecordingAnnotator {
|
|
private static final AtomicBoolean wait = new AtomicBoolean();
|
|
private static final AtomicBoolean running = new AtomicBoolean();
|
|
private static final String SWEARING = "No swearing";
|
|
|
|
@Override
|
|
public void annotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) {
|
|
if (running.getAndSet(true)) {
|
|
throw new IllegalStateException("Already running");
|
|
}
|
|
if (element instanceof PsiComment && element.getText().equals("//XXX")) {
|
|
while (wait.get()) {
|
|
Thread.onSpinWait();
|
|
}
|
|
holder.newAnnotation(HighlightSeverity.ERROR, SWEARING).range(element).create();
|
|
iDidIt();
|
|
}
|
|
LOG.debug(getClass()+".annotate("+element+") = "+didIDoIt());
|
|
running.set(false);
|
|
}
|
|
}
|
|
|
|
public void testTwoEditorsForTheSameDocumentDoNotCompeteForMarkupModelAndHighlightingDoesNotBlinkAfterModificationInSomeEditor() {
|
|
Set<Editor> applied = ConcurrentCollectionFactory.createConcurrentSet();
|
|
Set<Editor> collected = ConcurrentCollectionFactory.createConcurrentSet();
|
|
registerFakePass(applied, collected);
|
|
|
|
@Language("JAVA")
|
|
String text = """
|
|
class X {
|
|
//XXX
|
|
void foo() {
|
|
blahblah(); <caret>
|
|
}
|
|
}
|
|
""";
|
|
text = text.replaceAll("blahblah\\(\\); <caret>\n", "blahblah(); <caret>\n"+"blahblah();\n".repeat(1000));
|
|
assertTrue(text.length()>1000);
|
|
configureByText(JavaFileType.INSTANCE, text);
|
|
assertTrue(highlightErrors().size()>1000);// unresolved references
|
|
Editor editor1 = getEditor();
|
|
Editor editor2 = EditorFactory.getInstance().createEditor(editor1.getDocument(),getProject());
|
|
Disposer.register(getTestRootDisposable(), () -> EditorFactory.getInstance().releaseEditor(editor2));
|
|
TextEditor textEditor1 = new PsiAwareTextEditorProvider().getTextEditor(editor1);
|
|
TextEditor textEditor2 = new PsiAwareTextEditorProvider().getTextEditor(editor2);
|
|
assertNotNull(textEditor1);
|
|
assertNotNull(textEditor2);
|
|
EditorTracker.getInstance(getProject()).setActiveEditors(List.of(editor1, editor2));
|
|
|
|
// check that 'MySingletonAnnotator' is run only once for two editors for the same document
|
|
DaemonAnnotatorsRespondToChangesTest.useAnnotatorsIn(JavaFileType.INSTANCE.getLanguage(), new DaemonAnnotatorsRespondToChangesTest.MyRecordingAnnotator[]{new MySingletonAnnotator()}, ()-> {
|
|
for (int i=0; i<10; i++) {
|
|
MySingletonAnnotator.wait.set(true);
|
|
type("/");
|
|
PsiDocumentManager.getInstance(getProject()).commitAllDocuments();
|
|
AppExecutorUtil.getAppScheduledExecutorService().schedule(() -> MySingletonAnnotator.wait.set(false), 1000, TimeUnit.MILLISECONDS);
|
|
waitForDaemonToFinish(getProject(), editor1.getDocument());
|
|
|
|
// revert back
|
|
backspace();
|
|
PsiDocumentManager.getInstance(getProject()).commitAllDocuments();
|
|
waitForDaemonToFinish(getProject(), editor1.getDocument());
|
|
}
|
|
});
|
|
}
|
|
|
|
public void testHighlightingInSplittedWindowFinishesEventually() throws Exception {
|
|
myDaemonCodeAnalyzer.serializeCodeInsightPasses(true); // reproduced only for serialized passes
|
|
try {
|
|
Collection<Editor> applied = ContainerUtil.createConcurrentList();
|
|
Collection<Editor> collected = ContainerUtil.createConcurrentList();
|
|
registerFakePass(applied, collected);
|
|
|
|
@Language("JAVA")
|
|
String text = "class X {" + "\n".repeat(1000) +
|
|
"}";
|
|
configureByText(JavaFileType.INSTANCE, text);
|
|
Editor editor1 = getEditor();
|
|
Editor editor2 = EditorFactory.getInstance().createEditor(editor1.getDocument(),getProject());
|
|
Disposer.register(getTestRootDisposable(), () -> EditorFactory.getInstance().releaseEditor(editor2));
|
|
TextEditor textEditor1 = new PsiAwareTextEditorProvider().getTextEditor(editor1);
|
|
TextEditor textEditor2 = new PsiAwareTextEditorProvider().getTextEditor(editor2);
|
|
setActiveEditors(editor1, editor2);
|
|
|
|
myDaemonCodeAnalyzer.runPasses(myFile, editor1.getDocument(), textEditor1, new int[0], false, null);
|
|
myDaemonCodeAnalyzer.runPasses(myFile, editor1.getDocument(), textEditor2, new int[0], false, null);
|
|
|
|
assertSameElements(collected, Arrays.asList(editor1, editor2));
|
|
assertSameElements(applied, Arrays.asList(editor1, editor2));
|
|
|
|
applied.clear();
|
|
collected.clear();
|
|
setActiveEditors(editor1, editor2);
|
|
type("/* xxx */");
|
|
waitForDaemonToFinish(myProject, myEditor.getDocument());
|
|
|
|
assertSameElements(collected, Arrays.asList(editor1, editor2));
|
|
assertSameElements(applied, Arrays.asList(editor1, editor2));
|
|
}
|
|
finally {
|
|
myDaemonCodeAnalyzer.serializeCodeInsightPasses(false);
|
|
}
|
|
}
|
|
|
|
private void registerFakePass(@NotNull Collection<? super Editor> applied, @NotNull Collection<? super Editor> collected) {
|
|
class Fac implements TextEditorHighlightingPassFactory {
|
|
@Override
|
|
public TextEditorHighlightingPass createHighlightingPass(@NotNull PsiFile psiFile, @NotNull Editor editor) {
|
|
return new EditorBoundHighlightingPass(editor, psiFile, false) {
|
|
@Override
|
|
public void doCollectInformation(@NotNull ProgressIndicator progress) {
|
|
collected.add(editor);
|
|
}
|
|
|
|
@Override
|
|
public void doApplyInformationToEditor() {
|
|
applied.add(editor);
|
|
}
|
|
};
|
|
}
|
|
}
|
|
TextEditorHighlightingPassRegistrar registrar = TextEditorHighlightingPassRegistrar.getInstance(getProject());
|
|
registrar.registerTextEditorHighlightingPass(new Fac(), null, null, false, -1);
|
|
}
|
|
|
|
private volatile boolean runHeavyProcessing;
|
|
public void testDaemonDisablesItselfDuringHeavyProcessing() {
|
|
runWithReparseDelay(0, () -> {
|
|
runHeavyProcessing = false;
|
|
try {
|
|
Set<Editor> applied = Collections.synchronizedSet(new HashSet<>());
|
|
Set<Editor> collected = Collections.synchronizedSet(new HashSet<>());
|
|
registerFakePass(applied, collected);
|
|
|
|
configureByText(PlainTextFileType.INSTANCE, "");
|
|
Editor editor = getEditor();
|
|
EditorTracker editorTracker = EditorTracker.Companion.getInstance(myProject);
|
|
setActiveEditors(editor);
|
|
while (HeavyProcessLatch.INSTANCE.isRunning()) {
|
|
UIUtil.dispatchAllInvocationEvents();
|
|
}
|
|
type("xxx"); // restart daemon
|
|
assertTrue(editorTracker.getActiveEditors().contains(editor));
|
|
assertSame(editor, FileEditorManager.getInstance(myProject).getSelectedTextEditor());
|
|
|
|
|
|
// wait for the first pass to complete
|
|
long start = System.currentTimeMillis();
|
|
while (myDaemonCodeAnalyzer.isRunning() || !applied.contains(editor)) {
|
|
UIUtil.dispatchAllInvocationEvents();
|
|
if (System.currentTimeMillis() - start > 1000000) {
|
|
fail("Too long waiting for daemon (" +(System.currentTimeMillis() - start)+"ms) ");
|
|
}
|
|
}
|
|
|
|
runHeavyProcessing = true;
|
|
ApplicationManager.getApplication().executeOnPooledThread(() ->
|
|
HeavyProcessLatch.INSTANCE.performOperation(HeavyProcessLatch.Type.Processing, "my own heavy op", ()-> {
|
|
while (runHeavyProcessing) {
|
|
Thread.onSpinWait();
|
|
}
|
|
})
|
|
);
|
|
while (!HeavyProcessLatch.INSTANCE.isRunning()) {
|
|
UIUtil.dispatchAllInvocationEvents();
|
|
}
|
|
applied.clear();
|
|
collected.clear();
|
|
|
|
type("xxx"); // try to restart daemon
|
|
|
|
start = System.currentTimeMillis();
|
|
while (System.currentTimeMillis() < start + 5000) {
|
|
assertEmpty(applied); // it should not restart
|
|
assertEmpty(collected);
|
|
UIUtil.dispatchAllInvocationEvents();
|
|
}
|
|
}
|
|
finally {
|
|
runHeavyProcessing = false;
|
|
}
|
|
});
|
|
}
|
|
|
|
public void testDaemonDoesNotDisableItselfDuringVFSRefresh() {
|
|
runWithReparseDelay(0, () -> {
|
|
runHeavyProcessing = false;
|
|
try {
|
|
Set<Editor> applied = Collections.synchronizedSet(new HashSet<>());
|
|
Set<Editor> collected = Collections.synchronizedSet(new HashSet<>());
|
|
registerFakePass(applied, collected);
|
|
|
|
configureByText(PlainTextFileType.INSTANCE, "");
|
|
Editor editor = getEditor();
|
|
EditorTracker editorTracker = EditorTracker.Companion.getInstance(myProject);
|
|
setActiveEditors(editor);
|
|
while (HeavyProcessLatch.INSTANCE.isRunning()) {
|
|
UIUtil.dispatchAllInvocationEvents();
|
|
}
|
|
type("xxx"); // restart daemon
|
|
assertTrue(editorTracker.getActiveEditors().contains(editor));
|
|
assertSame(editor, FileEditorManager.getInstance(myProject).getSelectedTextEditor());
|
|
|
|
|
|
// wait for the first pass to complete
|
|
long start = System.currentTimeMillis();
|
|
while (myDaemonCodeAnalyzer.isRunning() || !applied.contains(editor)) {
|
|
UIUtil.dispatchAllInvocationEvents();
|
|
if (System.currentTimeMillis() - start > 1000000) {
|
|
fail("Too long waiting for daemon (" +(System.currentTimeMillis() - start)+"ms) ");
|
|
}
|
|
}
|
|
|
|
runHeavyProcessing = true;
|
|
Future<?> future = ApplicationManager.getApplication().executeOnPooledThread(() ->
|
|
HeavyProcessLatch.INSTANCE.performOperation(HeavyProcessLatch.Type.Syncing, "my own vfs refresh", () -> {
|
|
while (runHeavyProcessing) {
|
|
Thread.onSpinWait();
|
|
}
|
|
})
|
|
);
|
|
while (!HeavyProcessLatch.INSTANCE.isRunning()) {
|
|
UIUtil.dispatchAllInvocationEvents();
|
|
}
|
|
applied.clear();
|
|
collected.clear();
|
|
|
|
type("xxx"); // try to restart daemon
|
|
|
|
doHighlighting();
|
|
assertNotEmpty(applied); // it should restart
|
|
assertNotEmpty(collected);
|
|
runHeavyProcessing = false;
|
|
try {
|
|
future.get();
|
|
}
|
|
catch (Exception e) {
|
|
throw new RuntimeException(e);
|
|
}
|
|
}
|
|
finally {
|
|
runHeavyProcessing = false;
|
|
}
|
|
});
|
|
}
|
|
|
|
public void testDaemonMustDisableItselfDuringDocumentBulkModification() {
|
|
runWithReparseDelay(0, () -> {
|
|
configureByText(PlainTextFileType.INSTANCE, "");
|
|
Editor editor = getEditor();
|
|
|
|
Set<Editor> applied = Collections.synchronizedSet(new HashSet<>());
|
|
Set<Editor> collected = Collections.synchronizedSet(new HashSet<>());
|
|
DocumentUtil.executeInBulk(editor.getDocument(), () -> {
|
|
registerFakePass(applied, collected);
|
|
|
|
EditorTracker editorTracker = EditorTracker.Companion.getInstance(myProject);
|
|
setActiveEditors(editor);
|
|
assertTrue(editorTracker.getActiveEditors().contains(editor));
|
|
assertSame(editor, FileEditorManager.getInstance(myProject).getSelectedTextEditor());
|
|
|
|
applied.clear();
|
|
collected.clear();
|
|
|
|
myDaemonCodeAnalyzer.restart(getTestName(false)); // try to restart daemon
|
|
|
|
long start = System.currentTimeMillis();
|
|
while (System.currentTimeMillis() < start + 5000) {
|
|
assertEmpty(applied); // it must not restart
|
|
assertEmpty(collected);
|
|
UIUtil.dispatchAllInvocationEvents();
|
|
}
|
|
});
|
|
|
|
applied.clear();
|
|
collected.clear();
|
|
|
|
myDaemonCodeAnalyzer.restart(getTestName(false)); // try to restart daemon
|
|
|
|
long start = System.currentTimeMillis();
|
|
while (System.currentTimeMillis() < start + 5000 && applied.isEmpty()) {
|
|
UIUtil.dispatchAllInvocationEvents();
|
|
}
|
|
assertNotEmpty(applied); // it must restart outside bulk
|
|
assertNotEmpty(collected);
|
|
});
|
|
}
|
|
|
|
public void testModificationInsideCodeBlockDoesNotRehighlightWholeFile() {
|
|
configureByText(JavaFileType.INSTANCE, """
|
|
class X {
|
|
int f = "error";
|
|
int f() {
|
|
return 11<caret>;
|
|
}
|
|
}""");
|
|
HighlightInfo error = assertOneElement(highlightErrors());
|
|
assertEquals("Incompatible types. Found: 'java.lang.String', required: 'int'", error.getDescription());
|
|
|
|
error.getHighlighter().dispose();
|
|
|
|
assertEmpty(highlightErrors());
|
|
|
|
type("23");
|
|
assertEmpty(highlightErrors());
|
|
|
|
myEditor.getCaretModel().moveToOffset(0);
|
|
type("/* */");
|
|
HighlightInfo error2 = assertOneElement(highlightErrors());
|
|
assertEquals("Incompatible types. Found: 'java.lang.String', required: 'int'", error2.getDescription());
|
|
}
|
|
|
|
static void runWithReparseDelay(int reparseDelayMs, @NotNull Runnable task) {
|
|
DaemonCodeAnalyzerSettings settings = DaemonCodeAnalyzerSettings.getInstance();
|
|
int oldDelay = settings.getAutoReparseDelay();
|
|
settings.setAutoReparseDelay(reparseDelayMs);
|
|
try {
|
|
task.run();
|
|
}
|
|
finally {
|
|
settings.setAutoReparseDelay(oldDelay);
|
|
}
|
|
}
|
|
|
|
public void testCodeFoldingPassRestartsOnRegionUnfolding() {
|
|
runWithReparseDelay(0, () -> {
|
|
@Language("JAVA")
|
|
String text = """
|
|
class Foo {
|
|
void m() {
|
|
|
|
}
|
|
}""";
|
|
configureByText(JavaFileType.INSTANCE, text);
|
|
EditorTestUtil.buildInitialFoldingsInBackground(myEditor);
|
|
waitForDaemonToFinish(myProject, myEditor.getDocument());
|
|
EditorTestUtil.executeAction(myEditor, IdeActions.ACTION_COLLAPSE_ALL_REGIONS);
|
|
waitForDaemonToFinish(myProject, myEditor.getDocument());
|
|
checkFoldingState("[FoldRegion +(25:33), placeholder='{}']");
|
|
|
|
WriteCommandAction.runWriteCommandAction(myProject, () -> myEditor.getDocument().insertString(0, "/*"));
|
|
waitForDaemonToFinish(myProject, myEditor.getDocument());
|
|
checkFoldingState("[FoldRegion -(0:37), placeholder='/.../', FoldRegion +(27:35), placeholder='{}']");
|
|
|
|
EditorTestUtil.executeAction(myEditor, IdeActions.ACTION_EXPAND_ALL_REGIONS);
|
|
waitForDaemonToFinish(myProject, myEditor.getDocument());
|
|
checkFoldingState("[FoldRegion -(0:37), placeholder='/.../']");
|
|
});
|
|
}
|
|
|
|
public void testChangingSettingsHasImmediateEffectOnOpenedEditor() {
|
|
runWithReparseDelay(0, () -> {
|
|
@Language("JAVA")
|
|
String text = """
|
|
class C {\s
|
|
void m() {
|
|
}\s
|
|
}""";
|
|
configureByText(JavaFileType.INSTANCE, text);
|
|
EditorTestUtil.buildInitialFoldingsInBackground(myEditor);
|
|
waitForDaemonToFinish(myProject, myEditor.getDocument());
|
|
checkFoldingState("[FoldRegion -(22:27), placeholder='{}']");
|
|
|
|
JavaCodeFoldingSettings settings = JavaCodeFoldingSettings.getInstance();
|
|
boolean savedValue = settings.isCollapseMethods();
|
|
try {
|
|
settings.setCollapseMethods(true);
|
|
CodeFoldingConfigurable.Util.applyCodeFoldingSettingsChanges();
|
|
waitForDaemonToFinish(myProject, myEditor.getDocument());
|
|
checkFoldingState("[FoldRegion +(22:27), placeholder='{}']");
|
|
}
|
|
finally {
|
|
settings.setCollapseMethods(savedValue);
|
|
}
|
|
});
|
|
}
|
|
|
|
private void checkFoldingState(String expected) {
|
|
assertEquals(expected, Arrays.toString(myEditor.getFoldingModel().getAllFoldRegions()));
|
|
}
|
|
|
|
// return elapsed time in ms
|
|
static long waitForDaemonToFinish(@NotNull Project project, @NotNull Document document) {
|
|
ThreadingAssertions.assertEventDispatchThread();
|
|
long start = System.currentTimeMillis();
|
|
long deadline = start + 60_000;
|
|
waitForDaemonToStart(project, document, 60_000);
|
|
DaemonCodeAnalyzerImpl codeAnalyzer = (DaemonCodeAnalyzerImpl)DaemonCodeAnalyzer.getInstance(project);
|
|
while (daemonIsWorkingOrPending(project, document)) {
|
|
for (DaemonProgressIndicator indicator : codeAnalyzer.getUpdateProgress().values()) {
|
|
Throwable trace = indicator.getCancellationTrace();
|
|
if (trace != null && !(trace instanceof ProcessCanceledException)) {
|
|
ExceptionUtil.rethrow(trace);
|
|
}
|
|
}
|
|
if (System.currentTimeMillis() > deadline) {
|
|
DaemonRespondToChangesPerformanceTest.dumpThreadsToConsole();
|
|
fail("Too long waiting for daemon to finish ("+(System.currentTimeMillis()-start)+"ms already)");
|
|
}
|
|
PlatformTestUtil.dispatchAllInvocationEventsInIdeEventQueue();
|
|
}
|
|
return System.currentTimeMillis()-start;
|
|
}
|
|
|
|
private static void waitForDaemonToStart(@NotNull Project project, @NotNull Document document, long timeoutMs) {
|
|
PsiFile psiFile = PsiDocumentManager.getInstance(project).getPsiFile(document);
|
|
long deadline = System.currentTimeMillis() + timeoutMs;
|
|
DaemonCodeAnalyzerImpl myDaemonCodeAnalyzer = (DaemonCodeAnalyzerImpl)DaemonCodeAnalyzer.getInstance(project);
|
|
while (!myDaemonCodeAnalyzer.isRunning() && !myDaemonCodeAnalyzer.isAllAnalysisFinished(psiFile)) {
|
|
PlatformTestUtil.dispatchAllInvocationEventsInIdeEventQueue();
|
|
if (System.currentTimeMillis() > deadline) {
|
|
fail("Too long waiting for daemon to start (" +(System.currentTimeMillis() - deadline + timeoutMs)+"ms) "+
|
|
"daemonIsWorkingOrPending="+daemonIsWorkingOrPending(project, document)+
|
|
"; allFinished="+myDaemonCodeAnalyzer.isAllAnalysisFinished(psiFile)+
|
|
"; thread dump:\n------"+ThreadDumper.dumpThreadsToString()+"\n======");
|
|
}
|
|
}
|
|
}
|
|
|
|
static boolean daemonIsWorkingOrPending(@NotNull Project project, @NotNull Document document) {
|
|
DaemonCodeAnalyzerImpl codeAnalyzer = (DaemonCodeAnalyzerImpl)DaemonCodeAnalyzer.getInstance(project);
|
|
return codeAnalyzer.isRunningOrPending() || PsiDocumentManager.getInstance(project).isUncommited(document);
|
|
}
|
|
|
|
public void testRehighlightInDebuggerExpressionFragment() {
|
|
PsiExpressionCodeFragment fragment = JavaCodeFragmentFactory.getInstance(getProject()).createExpressionCodeFragment("+ <caret>\"a\"", null,
|
|
PsiType.getJavaLangObject(getPsiManager(), GlobalSearchScope.allScope(getProject())), true);
|
|
myFile = fragment;
|
|
Document document = Objects.requireNonNull(PsiDocumentManager.getInstance(getProject()).getDocument(fragment));
|
|
myEditor = EditorFactory.getInstance().createEditor(document, getProject(), JavaFileType.INSTANCE, false);
|
|
|
|
ProperTextRange visibleRange = makeEditorWindowVisible(new Point(0, 0), myEditor);
|
|
assertEquals(document.getTextLength(), visibleRange.getLength());
|
|
|
|
try {
|
|
EditorInfo editorInfo = new EditorInfo(document.getText());
|
|
|
|
String newFileText = editorInfo.getNewFileText();
|
|
ApplicationManager.getApplication().runWriteAction(() -> {
|
|
if (!document.getText().equals(newFileText)) {
|
|
document.setText(newFileText);
|
|
}
|
|
|
|
editorInfo.applyToEditor(myEditor);
|
|
});
|
|
|
|
PsiDocumentManager.getInstance(getProject()).commitAllDocuments();
|
|
|
|
|
|
HighlightInfo error = assertOneElement(highlightErrors());
|
|
assertEquals("Operator '+' cannot be applied to 'java.lang.String'", error.getDescription());
|
|
|
|
type(" ");
|
|
|
|
HighlightInfo after = assertOneElement(highlightErrors());
|
|
assertEquals("Operator '+' cannot be applied to 'java.lang.String'", after.getDescription());
|
|
}
|
|
finally {
|
|
EditorFactory.getInstance().releaseEditor(myEditor);
|
|
}
|
|
}
|
|
|
|
public void testDumbAwareHighlightingPassesStartEvenInDumbMode() {
|
|
List<TextEditorHighlightingPassFactory> collected = Collections.synchronizedList(new ArrayList<>());
|
|
List<TextEditorHighlightingPassFactory> applied = Collections.synchronizedList(new ArrayList<>());
|
|
class DumbFac implements TextEditorHighlightingPassFactory, DumbAware {
|
|
@Override
|
|
public TextEditorHighlightingPass createHighlightingPass(@NotNull PsiFile psiFile, @NotNull Editor editor) {
|
|
return new TestDumbAwareHighlightingPassesStartEvenInDumbModePass(editor, psiFile);
|
|
}
|
|
|
|
class TestDumbAwareHighlightingPassesStartEvenInDumbModePass extends EditorBoundHighlightingPass implements DumbAware {
|
|
TestDumbAwareHighlightingPassesStartEvenInDumbModePass(Editor editor, PsiFile psiFile) {
|
|
super(editor, psiFile, false);
|
|
}
|
|
|
|
@Override
|
|
public void doCollectInformation(@NotNull ProgressIndicator progress) {
|
|
collected.add(DumbFac.this);
|
|
}
|
|
|
|
@Override
|
|
public void doApplyInformationToEditor() {
|
|
applied.add(DumbFac.this);
|
|
}
|
|
}
|
|
}
|
|
TextEditorHighlightingPassRegistrar registrar = TextEditorHighlightingPassRegistrar.getInstance(getProject());
|
|
DumbFac dumbFac = new DumbFac();
|
|
registrar.registerTextEditorHighlightingPass(dumbFac, null, null, false, -1);
|
|
class SmartFac implements TextEditorHighlightingPassFactory {
|
|
@Override
|
|
public TextEditorHighlightingPass createHighlightingPass(@NotNull PsiFile psiFile, @NotNull Editor editor) {
|
|
return new EditorBoundHighlightingPass(editor, psiFile, false) {
|
|
@Override
|
|
public void doCollectInformation(@NotNull ProgressIndicator progress) {
|
|
if (DumbService.isDumb(myProject)) throw IndexNotReadyException.create();
|
|
collected.add(SmartFac.this);
|
|
}
|
|
|
|
@Override
|
|
public void doApplyInformationToEditor() {
|
|
if (DumbService.isDumb(myProject)) return;
|
|
applied.add(SmartFac.this);
|
|
}
|
|
};
|
|
}
|
|
}
|
|
SmartFac smartFac = new SmartFac();
|
|
registrar.registerTextEditorHighlightingPass(smartFac, null, null, false, -1);
|
|
|
|
configureByText(PlainTextFileType.INSTANCE, "");
|
|
doHighlighting();
|
|
assertSameElements(collected, dumbFac, smartFac);
|
|
assertSameElements(applied, dumbFac, smartFac);
|
|
collected.clear();
|
|
applied.clear();
|
|
|
|
myDaemonCodeAnalyzer.mustWaitForSmartMode(false, getTestRootDisposable());
|
|
DumbModeTestUtils.runInDumbModeSynchronously(myProject, () -> {
|
|
type(' ');
|
|
doHighlighting();
|
|
|
|
TextEditorHighlightingPassFactory f = assertOneElement(collected);
|
|
assertSame(dumbFac, f);
|
|
TextEditorHighlightingPassFactory f2 = assertOneElement(applied);
|
|
assertSame(dumbFac, f2);
|
|
});
|
|
}
|
|
|
|
public void testUncommittedByAccidentNonPhysicalDocumentMustNotHangDaemon() {
|
|
ThreadingAssertions.assertEventDispatchThread();
|
|
configureByText(JavaFileType.INSTANCE, "class X { void f() { <caret> } }");
|
|
assertEmpty(highlightErrors());
|
|
assertFalse(ApplicationManager.getApplication().isWriteAccessAllowed());
|
|
|
|
PsiFile original = getPsiManager().findFile(getTempDir().createVirtualFile("X.txt", ""));
|
|
assertNotNull(original);
|
|
assertTrue(original.getViewProvider().isEventSystemEnabled());
|
|
|
|
PsiFile copy = (PsiFile)original.copy();
|
|
assertFalse(copy.getViewProvider().isEventSystemEnabled());
|
|
|
|
Document document = copy.getViewProvider().getDocument();
|
|
assertNotNull(document);
|
|
String text = "class A{}";
|
|
document.setText(text);
|
|
assertFalse(PsiDocumentManager.getInstance(myProject).isCommitted(document));
|
|
assertTrue(PsiDocumentManager.getInstance(myProject).hasUncommitedDocuments());
|
|
|
|
type("String i=0;");
|
|
waitForDaemonToFinish(myProject, myEditor.getDocument());
|
|
assertNotEmpty(DaemonCodeAnalyzerImpl.getHighlights(getEditor().getDocument(), HighlightSeverity.ERROR, getProject()));
|
|
assertEquals(text, document.getText()); // retain non-phys document until after highlighting
|
|
assertFalse(PsiDocumentManager.getInstance(myProject).isCommitted(document));
|
|
assertTrue(PsiDocumentManager.getInstance(myProject).hasUncommitedDocuments());
|
|
}
|
|
|
|
public void testPutArgumentsOnSeparateLinesIntentionMustNotRemoveErrorHighlighting() {
|
|
configureByText(JavaFileType.INSTANCE, "class X{ static void foo(String s1, String s2, String s3) { foo(\"1\", 1.2, \"2\"<caret>); }}");
|
|
assertOneElement(highlightErrors());
|
|
|
|
List<IntentionAction> fixes = LightQuickFixTestCase.getAvailableActions(getEditor(), getFile());
|
|
IntentionAction intention = assertContainsOneOf(fixes, "Put arguments on separate lines");
|
|
assertNotNull(intention);
|
|
|
|
WriteCommandAction.runWriteCommandAction(getProject(), () -> intention.invoke(getProject(), getEditor(), getFile()));
|
|
assertOneElement(highlightErrors());
|
|
}
|
|
|
|
|
|
public void testHighlightingPassesAreInstantiatedOutsideEDTToImproveResponsiveness() throws Throwable {
|
|
AtomicReference<Throwable> violation = new AtomicReference<>();
|
|
AtomicBoolean applied = new AtomicBoolean();
|
|
class MyCheckingConstructorTraceFac implements TextEditorHighlightingPassFactory {
|
|
@Override
|
|
public TextEditorHighlightingPass createHighlightingPass(@NotNull PsiFile psiFile, @NotNull Editor editor) {
|
|
return new TestHighlightingPassesAreInstantiatedOutsideEDTToImproveResponsivenessPass(myProject);
|
|
}
|
|
|
|
final class TestHighlightingPassesAreInstantiatedOutsideEDTToImproveResponsivenessPass extends TextEditorHighlightingPass {
|
|
private TestHighlightingPassesAreInstantiatedOutsideEDTToImproveResponsivenessPass(Project project) {
|
|
super(project, getEditor().getDocument(), false);
|
|
if (ApplicationManager.getApplication().isDispatchThread()) {
|
|
violation.set(new Throwable());
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void doCollectInformation(@NotNull ProgressIndicator progress) {
|
|
}
|
|
|
|
@Override
|
|
public void doApplyInformationToEditor() {
|
|
applied.set(true);
|
|
}
|
|
}
|
|
}
|
|
TextEditorHighlightingPassRegistrar registrar = TextEditorHighlightingPassRegistrar.getInstance(getProject());
|
|
registrar.registerTextEditorHighlightingPass(new MyCheckingConstructorTraceFac(), null, null, false, -1);
|
|
configureByText(JavaFileType.INSTANCE, "class C{}");
|
|
assertEmpty(highlightErrors());
|
|
assertTrue(applied.get());
|
|
if (violation.get() != null) {
|
|
throw violation.get();
|
|
}
|
|
}
|
|
|
|
private static class EmptyPassFactory implements TextEditorHighlightingPassFactory {
|
|
@Override
|
|
public TextEditorHighlightingPass createHighlightingPass(@NotNull PsiFile psiFile, @NotNull Editor editor) {
|
|
return new EmptyPass(psiFile.getProject(), editor.getDocument());
|
|
}
|
|
|
|
static class EmptyPass extends TextEditorHighlightingPass {
|
|
private EmptyPass(Project project, @NotNull Document document) {
|
|
super(project, document, false);
|
|
}
|
|
|
|
@Override
|
|
public void doCollectInformation(@NotNull ProgressIndicator progress) {
|
|
}
|
|
|
|
@Override
|
|
public void doApplyInformationToEditor() {
|
|
}
|
|
}
|
|
}
|
|
|
|
public void testTextEditorHighlightingPassRegistrarMustNotAllowCyclesInPassDeclarationsOrCrazyPassIdsOmgMurphyLawStrikesAgain() {
|
|
configureByText(JavaFileType.INSTANCE, "class C{}");
|
|
TextEditorHighlightingPassRegistrarImpl registrar = (TextEditorHighlightingPassRegistrarImpl)TextEditorHighlightingPassRegistrar.getInstance(getProject());
|
|
int F2 = Pass.EXTERNAL_TOOLS;
|
|
int forcedId1 = 256;
|
|
int forcedId2 = 257;
|
|
int forcedId3 = 258;
|
|
assertThrows(IllegalArgumentException.class, () ->
|
|
// afterCompletionOf and afterStartingOf must not intersect
|
|
registrar.registerTextEditorHighlightingPass(new EmptyPassFactory(), new int[]{F2}, new int[]{F2}, false, -1));
|
|
assertThrows(IllegalArgumentException.class, () ->
|
|
// afterCompletionOf and afterStartingOf must not contain forcedId
|
|
registrar.registerTextEditorHighlightingPass(new EmptyPassFactory(), new int[]{forcedId1}, null, false, forcedId1));
|
|
assertThrows(IllegalArgumentException.class, () ->
|
|
// afterCompletionOf and afterStartingOf must not contain forcedId
|
|
registrar.registerTextEditorHighlightingPass(new EmptyPassFactory(), null, new int[]{forcedId1}, false, forcedId1));
|
|
assertThrows(IllegalArgumentException.class, () ->
|
|
// afterCompletionOf and afterStartingOf must not contain crazy ids
|
|
registrar.registerTextEditorHighlightingPass(new EmptyPassFactory(), new int[]{0}, new int[]{F2}, false, forcedId1));
|
|
assertThrows(IllegalArgumentException.class, () ->
|
|
// afterCompletionOf and afterStartingOf must not contain crazy ids
|
|
registrar.registerTextEditorHighlightingPass(new EmptyPassFactory(), new int[]{-1}, new int[]{F2}, false, forcedId1));
|
|
assertThrows(IllegalArgumentException.class, () ->
|
|
// afterCompletionOf and afterStartingOf must not contain crazy ids
|
|
registrar.registerTextEditorHighlightingPass(new EmptyPassFactory(), new int[]{F2}, new int[]{0}, false, forcedId1));
|
|
assertThrows(IllegalArgumentException.class, () ->
|
|
// afterCompletionOf and afterStartingOf must not contain crazy ids
|
|
registrar.registerTextEditorHighlightingPass(new EmptyPassFactory(), new int[]{F2}, new int[]{-1}, false, forcedId1));
|
|
assertThrows(IllegalArgumentException.class, () ->
|
|
// afterCompletionOf and afterStartingOf must not contain crazy ids
|
|
registrar.registerTextEditorHighlightingPass(new EmptyPassFactory(), new int[]{32134}, new int[]{F2}, false, forcedId1));
|
|
assertThrows(IllegalArgumentException.class, () ->
|
|
// afterCompletionOf and afterStartingOf must not contain crazy ids
|
|
registrar.registerTextEditorHighlightingPass(new EmptyPassFactory(), new int[]{F2}, new int[]{982314}, false, forcedId1));
|
|
|
|
assertThrows(IllegalArgumentException.class, () -> {
|
|
registrar.registerTextEditorHighlightingPass(new EmptyPassFactory(), new int[]{forcedId2}, null, false, forcedId1);
|
|
registrar.registerTextEditorHighlightingPass(new EmptyPassFactory(), new int[]{forcedId1}, null, false, forcedId2);
|
|
assertEmpty(highlightErrors());
|
|
});
|
|
// non-direct cycle
|
|
assertThrows(IllegalArgumentException.class, () -> {
|
|
registrar.reRegisterFactories(); // clear caches from incorrect factories above
|
|
registrar.registerTextEditorHighlightingPass(new EmptyPassFactory(), new int[]{forcedId2}, null, false, forcedId1);
|
|
registrar.registerTextEditorHighlightingPass(new EmptyPassFactory(), new int[]{forcedId3}, null, false, forcedId2);
|
|
registrar.registerTextEditorHighlightingPass(new EmptyPassFactory(), new int[]{forcedId1}, null, false, forcedId3);
|
|
assertEmpty(highlightErrors());
|
|
});
|
|
|
|
registrar.reRegisterFactories(); // clear caches from incorrect factories above
|
|
registrar.registerTextEditorHighlightingPass(new EmptyPassFactory(), null, null, false, forcedId1);
|
|
registrar.registerTextEditorHighlightingPass(new EmptyPassFactory(), new int[]{forcedId1}, null, false, forcedId3);
|
|
registrar.registerTextEditorHighlightingPass(new EmptyPassFactory(), new int[]{forcedId3}, null, false, forcedId2);
|
|
assertEmpty(highlightErrors());
|
|
}
|
|
|
|
public void testHighlightersMustDisappearWhenTheHighlightingIsSwitchedOff() {
|
|
@Language("JAVA")
|
|
String text = """
|
|
class X {
|
|
blah blah
|
|
)(@*$)(*%@$)
|
|
}""";
|
|
configureByText(JavaFileType.INSTANCE, text);
|
|
assertNotEmpty(highlightErrors());
|
|
HighlightingSettingsPerFile.getInstance(getProject()).setHighlightingSettingForRoot(getFile(), FileHighlightingSetting.SKIP_HIGHLIGHTING);
|
|
|
|
assertEmpty(highlightErrors());
|
|
}
|
|
|
|
public void testTypingErrorElementMustHighlightIt() {
|
|
ThreadingAssertions.assertEventDispatchThread();
|
|
configureByText(JavaFileType.INSTANCE, "class X { void f() { } }<caret>");
|
|
assertEmpty(highlightErrors());
|
|
makeEditorWindowVisible(new Point(0, 1000), myEditor);
|
|
|
|
type("/");
|
|
waitForDaemonToFinish(myProject, myEditor.getDocument());
|
|
List<HighlightInfo> errors = DaemonCodeAnalyzerImpl.getHighlights(getEditor().getDocument(), HighlightSeverity.ERROR, getProject());
|
|
assertNotEmpty(errors);
|
|
assertTrue(errors.toString().contains("'class' or 'interface' expected"));
|
|
}
|
|
|
|
public void testTypingInsideCodeBlockCanAffectUnusedDeclarationInTheOtherClass() {
|
|
enableInspectionTool(new UnusedSymbolLocalInspection());
|
|
enableDeadCodeInspection();
|
|
configureByFiles(null, BASE_PATH+getTestName(true)+"/p2/A2222.java", BASE_PATH+getTestName(true)+"/p1/A1111.java");
|
|
assertEquals("A2222.java", getFile().getName());
|
|
|
|
HighlightInfo info = assertOneElement(doHighlighting(HighlightSeverity.WARNING));
|
|
assertEquals("Class 'A2222' is never used", info.getDescription());
|
|
|
|
Document document1111 = getFile().getParent().findFile("A1111.java").getFileDocument();
|
|
// uncomment (inside code block) the reference to A2222
|
|
WriteCommandAction.writeCommandAction(myProject).run(()->document1111.deleteString(document1111.getText().indexOf("//"), document1111.getText().indexOf("//")+2));
|
|
|
|
// now A2222 is no longer unused
|
|
assertEmpty(doHighlighting(HighlightSeverity.WARNING));
|
|
}
|
|
|
|
// test the other type of PSI change: child remove/child add
|
|
public void testTypingInsideCodeBlockCanAffectUnusedDeclarationInTheOtherClass2() {
|
|
enableInspectionTool(new UnusedSymbolLocalInspection());
|
|
enableDeadCodeInspection();
|
|
configureByFiles(null, BASE_PATH+getTestName(true)+"/p1/A1111.java", BASE_PATH+getTestName(true)+"/p2/A2222.java");
|
|
assertEquals("A1111.java", getFile().getName());
|
|
makeEditorWindowVisible(new Point(0, 1000), myEditor);
|
|
HighlightInfo info = assertOneElement(doHighlighting(HighlightSeverity.WARNING));
|
|
assertEquals("Method 'foo()' is never used", info.getDescription());
|
|
|
|
Document document2222 = getFile().getParent().findFile("A2222.java").getFileDocument();
|
|
// uncomment (inside code block) the reference to A1111
|
|
WriteCommandAction.writeCommandAction(myProject).run(()->document2222.deleteString(document2222.getText().indexOf("//"), document2222.getText().indexOf("//")+2));
|
|
|
|
// now foo() is no longer unused
|
|
assertEmpty(doHighlighting(HighlightSeverity.WARNING));
|
|
}
|
|
|
|
public void testTypingDoesNotLeaveInvalidPSIShitBehind() {
|
|
String text = """
|
|
class X {
|
|
void f() {
|
|
///
|
|
int s;<caret>
|
|
///
|
|
}
|
|
}""";
|
|
String bigText = text.replaceAll("///\n", "hashCode();\n".repeat(1000));
|
|
configureByText(JavaFileType.INSTANCE, bigText);
|
|
makeEditorWindowVisible(new Point(0, 1000), myEditor);
|
|
assertEmpty(doHighlighting(HighlightSeverity.ERROR));
|
|
MarkupModel markupModel = DocumentMarkupModel.forDocument(myEditor.getDocument(), getProject(), true);
|
|
for (int i=0; i<10; i++) {
|
|
type(" // TS");
|
|
assertEmpty(doHighlighting(HighlightSeverity.ERROR));
|
|
assertEmpty(getErrorsFromMarkup(markupModel));
|
|
|
|
backspace();backspace();backspace();backspace();backspace();backspace();
|
|
assertEmpty(doHighlighting(HighlightSeverity.ERROR));
|
|
assertEmpty(getErrorsFromMarkup(markupModel));
|
|
}
|
|
}
|
|
|
|
// highlights all //XXX, but very slow
|
|
public static class MyVerySlowAnnotator extends DaemonAnnotatorsRespondToChangesTest.MyRecordingAnnotator {
|
|
private static final AtomicBoolean wait = new AtomicBoolean();
|
|
private static final String SWEARING = "No swearing";
|
|
|
|
@Override
|
|
public void annotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) {
|
|
if (element instanceof PsiComment && element.getText().equals("//XXX")) {
|
|
while (wait.get()) {
|
|
Thread.onSpinWait();
|
|
}
|
|
holder.newAnnotation(HighlightSeverity.ERROR, SWEARING).range(element).create();
|
|
iDidIt();
|
|
}
|
|
LOG.debug(getClass()+".annotate("+element+") = "+didIDoIt());
|
|
}
|
|
static List<HighlightInfo> myHighlights(MarkupModel markupModel) {
|
|
return Arrays.stream(markupModel.getAllHighlighters())
|
|
.map(highlighter -> HighlightInfo.fromRangeHighlighter(highlighter))
|
|
.filter(Objects::nonNull)
|
|
.filter(info -> SWEARING.equals(info.getDescription())).toList();
|
|
}
|
|
static List<HighlightInfo> syntaxHighlights(MarkupModel markupModel, String description) {
|
|
return Arrays.stream(markupModel.getAllHighlighters())
|
|
.map(highlighter -> HighlightInfo.fromRangeHighlighter(highlighter))
|
|
.filter(Objects::nonNull)
|
|
.filter(info -> description.equals(info.getDescription())).toList();
|
|
}
|
|
}
|
|
|
|
public void testInvalidPSIElementsCreatedByTypingNearThemMustBeRemovedImmediatelyMeaningLongBeforeTheHighlightingPassFinished() {
|
|
@Language("JAVA")
|
|
String text = """
|
|
class X {
|
|
void f() {
|
|
//XXX
|
|
|
|
int s-<caret>; // ';' expected
|
|
|
|
}
|
|
}""";
|
|
|
|
assertInvalidPSIElementHighlightingIsRemovedImmediatelyAfterRepairingChange(text, "';' expected", () -> backspace());
|
|
}
|
|
|
|
public void testInvalidPSIElementsCreatedByTypingNearThemMustBeRemovedImmediatelyMeaningLongBeforeTheHighlightingPassFinished2() {
|
|
@Language("JAVA")
|
|
String text = """
|
|
class X {
|
|
void f() {
|
|
//XXX
|
|
|
|
<caret> : // Unexpected token
|
|
|
|
}
|
|
}""";
|
|
|
|
assertInvalidPSIElementHighlightingIsRemovedImmediatelyAfterRepairingChange(text, "Unexpected token", () -> type("//"));
|
|
}
|
|
|
|
private void assertInvalidPSIElementHighlightingIsRemovedImmediatelyAfterRepairingChange(@Language("JAVA") String text,
|
|
String errorDescription,
|
|
Runnable repairingChange // the change which is supposed to fix the invalid PSI highlight
|
|
) {
|
|
configureByText(JavaFileType.INSTANCE, text);
|
|
makeEditorWindowVisible(new Point(0, 1000), myEditor);
|
|
MyVerySlowAnnotator.wait.set(false);
|
|
DaemonAnnotatorsRespondToChangesTest.useAnnotatorsIn(JavaFileType.INSTANCE.getLanguage(), new DaemonAnnotatorsRespondToChangesTest.MyRecordingAnnotator[]{new MyVerySlowAnnotator()}, ()->{
|
|
MarkupModel markupModel = DocumentMarkupModel.forDocument(getEditor().getDocument(), getProject(), true);
|
|
doHighlighting();
|
|
assertNotEmpty(MyVerySlowAnnotator.syntaxHighlights(markupModel, errorDescription));
|
|
assertNotEmpty(MyVerySlowAnnotator.myHighlights(markupModel));
|
|
|
|
MyVerySlowAnnotator.wait.set(true);
|
|
repairingChange.run(); //repair invalid psi
|
|
AtomicBoolean success = new AtomicBoolean();
|
|
// register very slow annotator and make sure the invalid PSI highlighting was removed before this annotator finished
|
|
TestTimeOut n = TestTimeOut.setTimeout(100, TimeUnit.SECONDS);
|
|
Runnable checkHighlighted = () -> {
|
|
UIUtil.dispatchAllInvocationEvents();
|
|
if (MyVerySlowAnnotator.syntaxHighlights(markupModel, errorDescription).isEmpty() && MyVerySlowAnnotator.wait.get()) {
|
|
// removed before highlighting is finished
|
|
MyVerySlowAnnotator.wait.set(false);
|
|
success.set(true);
|
|
}
|
|
if (n.isTimedOut()) {
|
|
MyVerySlowAnnotator.wait.set(false);
|
|
throw new RuntimeException(new TimeoutException(ThreadDumper.dumpThreadsToString()));
|
|
}
|
|
};
|
|
TextEditor textEditor = TextEditorProvider.getInstance().getTextEditor(getEditor());
|
|
try {
|
|
myDaemonCodeAnalyzer.runPasses(myFile, myEditor.getDocument(), textEditor, new int[0], false, checkHighlighted);
|
|
}
|
|
catch (Exception e) {
|
|
throw new RuntimeException(e);
|
|
}
|
|
|
|
assertEmpty(MyVerySlowAnnotator.syntaxHighlights(markupModel, errorDescription));
|
|
assertNotEmpty(MyVerySlowAnnotator.myHighlights(markupModel));
|
|
assertTrue(success.get());
|
|
});
|
|
}
|
|
|
|
@SuppressWarnings("FieldMayBeStatic")
|
|
@Language(value = "JAVA", prefix="class X { void foo() {\n", suffix = "\n}\n}")
|
|
private final String MANY_LAMBDAS_TEXT_TO_TYPE = """
|
|
if (i(()->{
|
|
i(()-> {
|
|
System.out.println("vFile = ");
|
|
});
|
|
})) {
|
|
i(new Runnable() {
|
|
@Override public void run() {
|
|
if (true==true) { return;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
else {
|
|
// return this
|
|
}
|
|
""";
|
|
|
|
@SuppressWarnings("FieldMayBeStatic")
|
|
@Language("JAVA")
|
|
private final String MANY_LAMBDAS_INITIAL = """
|
|
class X {
|
|
void invokeLater(Runnable r) {}
|
|
boolean i(Runnable r) { return true;}
|
|
void foo() {
|
|
<caret>
|
|
}
|
|
}""";
|
|
public void testDaemonDoesRestartDuringMadMonkeyTyping/*Stress*/() {
|
|
assertDaemonRestartsAndLeavesNoErrorElementsInTheEnd(MANY_LAMBDAS_INITIAL, MANY_LAMBDAS_TEXT_TO_TYPE, null);
|
|
}
|
|
@SuppressWarnings("FieldMayBeStatic")
|
|
@Language(value = "JAVA", prefix="class X { void foo() {\n", suffix = "\n}\n}")
|
|
private final String LONG_LINE_WITH_PARENS_TEXT_TO_TYPE = """
|
|
if (highlighter != null) highlighter += " text='" + StringUtil.first(getText(), 40, true) + "'";
|
|
""";
|
|
@SuppressWarnings("FieldMayBeStatic")
|
|
@Language("JAVA")
|
|
private final String LONG_LINE_WITH_PARENS_INITIAL_TEXT = """
|
|
class X {
|
|
static String getText() { return ""; }
|
|
static class StringUtil {
|
|
static String first(String t, int length, boolean b) { return t; }
|
|
}
|
|
String highlighter;
|
|
void foo() {
|
|
<caret>
|
|
}
|
|
}""";
|
|
public void testDaemonDoesRestartDuringMadMonkeyTyping2/*Stress*/() {
|
|
assertDaemonRestartsAndLeavesNoErrorElementsInTheEnd(LONG_LINE_WITH_PARENS_INITIAL_TEXT, LONG_LINE_WITH_PARENS_TEXT_TO_TYPE, null);
|
|
}
|
|
|
|
public void testDaemonDoesNotLeaveObsoleteErrorElementHighlightsBehind/*Stress*/() {
|
|
Random random = new Random();
|
|
assertDaemonRestartsAndLeavesNoErrorElementsInTheEnd(MANY_LAMBDAS_INITIAL, MANY_LAMBDAS_TEXT_TO_TYPE, () -> TimeoutUtil.sleep(random.nextInt(10)));
|
|
}
|
|
public void testDaemonDoesNotLeaveObsoleteErrorElementHighlightsBehind2/*Stress*/() {
|
|
Random random = new Random();
|
|
assertDaemonRestartsAndLeavesNoErrorElementsInTheEnd(LONG_LINE_WITH_PARENS_INITIAL_TEXT, LONG_LINE_WITH_PARENS_TEXT_TO_TYPE, () -> TimeoutUtil.sleep(random.nextInt(10)));
|
|
}
|
|
|
|
// start typing in the empty java file char by char
|
|
// after each typing, wait for the daemon to start and immediately proceed to type the next char
|
|
// thus making daemon interrupt itself constantly, in hope for multiple highlighting sessions overlappings to manifest themselves more quickly.
|
|
// after all typings are over, wait for final highlighting to complete and check that no errors are left in the markup
|
|
private void assertDaemonRestartsAndLeavesNoErrorElementsInTheEnd(String initialText, String textToType, Runnable afterWaitForDaemonToStart) {
|
|
// run expensive consistency checks on each typing
|
|
HighlightInfoUpdaterImpl updater = (HighlightInfoUpdaterImpl)HighlightInfoUpdaterImpl.getInstance(getProject());
|
|
updater.runAssertingInvariants(() -> {
|
|
String finalText = initialText.replace("<caret>", textToType);
|
|
configureByText(JavaFileType.INSTANCE, finalText);
|
|
assertEmpty(doHighlighting(HighlightSeverity.ERROR));
|
|
runWithReparseDelay(0, () -> {
|
|
for (int i=0; i<10; i++) {
|
|
//System.out.println("i = " + i);
|
|
PassExecutorService.LOG.debug("i = " + i);
|
|
WriteCommandAction.runWriteCommandAction(getProject(), () -> myEditor.getDocument().setText(" "));
|
|
doHighlighting(); // reset various optimizations e.g. FileStatusMap.getCompositeDocumentDirtyRange
|
|
MarkupModel markupModel = DocumentMarkupModel.forDocument(myEditor.getDocument(), getProject(), true);
|
|
for (int c = 0; c < finalText.length(); c++) {
|
|
PassExecutorService.LOG.debug("c = " + c);
|
|
//System.out.println(" c = " + c);
|
|
int o=c;
|
|
//updater.assertNoDuplicates(myFile, getErrorsFromMarkup(markupModel), "errors from markup ");
|
|
WriteCommandAction.runWriteCommandAction(getProject(), () -> {
|
|
assertFalse(myDaemonCodeAnalyzer.isRunning());
|
|
long docStamp = myEditor.getDocument().getModificationStamp();
|
|
char charToType = finalText.charAt(o);
|
|
type(charToType);
|
|
if (docStamp != myEditor.getDocument().getModificationStamp()) { // condition could be false when type handler does overtype ')' with already existing ')'
|
|
assertFalse(myDaemonCodeAnalyzer.isAllAnalysisFinished(myFile));
|
|
}
|
|
});
|
|
//updater.assertNoDuplicates(myFile, getErrorsFromMarkup(markupModel), "errors from markup ");
|
|
myDaemonCodeAnalyzer.restart(myFile);
|
|
List<HighlightInfo> errorsFromMarkup = getErrorsFromMarkup(markupModel);
|
|
//updater.assertNoDuplicates(myFile, errorsFromMarkup, "errors from markup ");
|
|
//((HighlightInfoUpdaterImpl)HighlightInfoUpdater.getInstance(getProject())).assertMarkupDataConsistent(myFile);
|
|
PassExecutorService.LOG.debug(" errorsfrommarkup:\n" + StringUtil.join(ContainerUtil.sorted(errorsFromMarkup, Segment.BY_START_OFFSET_THEN_END_OFFSET), "\n") + "\n-----\n");
|
|
waitForDaemonToStart(getProject(), myEditor.getDocument(), 30*1000);
|
|
if (afterWaitForDaemonToStart != null) {
|
|
afterWaitForDaemonToStart.run();
|
|
}
|
|
}
|
|
// some chars might be inserted by TypeHandlers
|
|
while (!myEditor.getDocument().getText().substring(myEditor.getCaretModel().getOffset()).isEmpty()) {
|
|
delete(myEditor);
|
|
}
|
|
LOG.debug("All typing completed. " +
|
|
"\neditor text:-----------\n"+myEditor.getDocument().getText()+"\n-------\n"+
|
|
"errors in markup: " + StringUtil.join(getErrorsFromMarkup(markupModel), "\n") + "\n-----\n");
|
|
waitForDaemonToFinish(getProject(), myEditor.getDocument());
|
|
assertEmpty(myEditor.getDocument().getText(), getErrorsFromMarkup(markupModel));
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
private static @NotNull List<HighlightInfo> getErrorsFromMarkup(@NotNull MarkupModel model) {
|
|
return Arrays.stream(model.getAllHighlighters())
|
|
.map(m -> HighlightInfo.fromRangeHighlighter(m))
|
|
.filter(Objects::nonNull)
|
|
.filter(h -> h.getSeverity() == HighlightSeverity.ERROR)
|
|
.toList();
|
|
}
|
|
private static void assertNoDuplicateInfosFromMarkup(@NotNull MarkupModel model) {
|
|
List<HighlightInfo> infos = Arrays.stream(model.getAllHighlighters())
|
|
.map(m -> HighlightInfo.fromRangeHighlighter(m))
|
|
.filter(Objects::nonNull)
|
|
.toList();
|
|
Map<TextRange, List<HighlightInfo>> byRange = infos.stream().collect(Collectors.groupingBy(info -> TextRange.create(info)));
|
|
for (List<HighlightInfo> errors : byRange.values()) {
|
|
Set<String> set = ContainerUtil.map2Set(errors, e -> e.getDescription());
|
|
if (set.size() != errors.size()) {
|
|
fail("Duplicates: " + errors);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void testMultiplePSIInvalidationsMustDelayTheirHighlightersRemovalForShortTimeToAvoidFlickering() {
|
|
//IJPL-160136 Blinking highlighting on refactoring TS code
|
|
|
|
@Language("JAVA")
|
|
String text = """
|
|
class X {
|
|
int xxx;
|
|
void foo() {
|
|
for (int i=0; i<xxx+1; i++) {
|
|
if (i == xxx) return xxx;
|
|
}
|
|
}
|
|
public int hashCode() {
|
|
return xxx;
|
|
}
|
|
}""";
|
|
|
|
DaemonAnnotatorsRespondToChangesTest.useAnnotatorsIn(JavaFileType.INSTANCE.getLanguage(), new DaemonAnnotatorsRespondToChangesTest.MyRecordingAnnotator[]{new MyXXXIdentifierAnnotator()}, ()->{
|
|
configureByText(JavaFileType.INSTANCE, text);
|
|
makeEditorWindowVisible(new Point(0, 1000), myEditor);
|
|
|
|
MarkupModelEx markupModel = (MarkupModelEx)DocumentMarkupModel.forDocument(getEditor().getDocument(), getProject(), true);
|
|
doHighlighting();
|
|
assertSize(5, MyXXXIdentifierAnnotator.myHighlights(markupModel));
|
|
|
|
List<String> events = Collections.synchronizedList(new ArrayList<>());
|
|
markupModel.addMarkupModelListener(getTestRootDisposable(), new MarkupModelListener() {
|
|
@Override
|
|
public void afterAdded(@NotNull RangeHighlighterEx highlighter) {
|
|
events.add("added " + highlighter);
|
|
}
|
|
|
|
@Override
|
|
public void afterRemoved(@NotNull RangeHighlighterEx highlighter) {
|
|
events.add("removed " + highlighter);
|
|
}
|
|
});
|
|
// invalidate all xxx (leaving the text the same), check that these highlighters are recycled
|
|
List<PsiIdentifier> identifiers = new ArrayList<>();
|
|
getFile().accept(new JavaRecursiveElementWalkingVisitor() {
|
|
@Override
|
|
public void visitIdentifier(@NotNull PsiIdentifier identifier) {
|
|
super.visitIdentifier(identifier);
|
|
if (identifier.getText().equals("xxx")) identifiers.add(identifier);
|
|
}
|
|
});
|
|
for (PsiIdentifier identifier : identifiers) {
|
|
WriteCommandAction.writeCommandAction(getProject()).run(() -> identifier.replace(PsiElementFactory.getInstance(getProject()).createIdentifier("xxx")));
|
|
assertFalse(identifier.isValid());
|
|
}
|
|
doHighlighting();
|
|
assertEmpty(events);
|
|
});
|
|
}
|
|
// highlight all identifiers with text "xxx"
|
|
public static class MyXXXIdentifierAnnotator extends DaemonAnnotatorsRespondToChangesTest.MyRecordingAnnotator {
|
|
private static final String MSG = "xxx?";
|
|
|
|
@Override
|
|
public void annotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) {
|
|
if (element instanceof PsiIdentifier && element.getText().equals("xxx")) {
|
|
holder.newAnnotation(HighlightSeverity.ERROR, MSG).range(element).create();
|
|
iDidIt();
|
|
}
|
|
LOG.debug(getClass()+".annotate("+element+") = "+didIDoIt());
|
|
}
|
|
static List<HighlightInfo> myHighlights(MarkupModel markupModel) {
|
|
return Arrays.stream(markupModel.getAllHighlighters())
|
|
.map(highlighter -> HighlightInfo.fromRangeHighlighter(highlighter))
|
|
.filter(Objects::nonNull)
|
|
.filter(info -> MSG.equals(info.getDescription())).toList();
|
|
}
|
|
}
|
|
|
|
public void testDaemonRestartsEventWhenCanceledDuringRunUpdateMethodCallIsRunning() {
|
|
configureByText(JavaFileType.INSTANCE, """
|
|
class AClass<caret> {
|
|
|
|
}
|
|
""");
|
|
Document document = getDocument(getFile());
|
|
assertEmpty(highlightErrors());
|
|
runWithReparseDelay(0, () -> {
|
|
// warmup highlighting first, calibrating before that would make little sense
|
|
for (int i = 0; i < 10; i++) {
|
|
type("x");
|
|
waitForDaemonToFinish(getProject(), document);
|
|
backspace();
|
|
PsiDocumentManager.getInstance(getProject()).commitAllDocuments();
|
|
}
|
|
long avgElapsedTime = 0;
|
|
int CALIBRATE_N = 100;
|
|
// compute time the highlighting takes to highlight this file completely
|
|
for (int i = 0; i < CALIBRATE_N; i++) {
|
|
type("x");
|
|
long elapsed = waitForDaemonToFinish(getProject(), document);
|
|
avgElapsedTime += elapsed;
|
|
backspace();
|
|
PsiDocumentManager.getInstance(getProject()).commitAllDocuments();
|
|
}
|
|
|
|
avgElapsedTime /= CALIBRATE_N; // compute avg time the daemon takes to highlight this sample. then we use that time to delay in hope that DAI.runUpdate() is about to run
|
|
LOG.debug("avgElapsedTime = " + avgElapsedTime);
|
|
|
|
Random random = new Random();
|
|
for (int i = 0; i < 1000; i++) {
|
|
LOG.debug("i = " + i);
|
|
type("x");
|
|
long delay = random.nextLong(avgElapsedTime+1);
|
|
Future<?> future = AppExecutorUtil.getAppScheduledExecutorService().schedule(() -> {
|
|
myDaemonCodeAnalyzer.restart(getTestName(false));
|
|
//String edtTrace = ThreadDumper.dumpEdtStackTrace(ThreadDumper.getThreadInfos());
|
|
//System.out.println(edtTrace+"\n delay ="+delay+" --------------------------------");
|
|
}, delay, TimeUnit.MILLISECONDS);
|
|
while (!future.isDone()) {
|
|
UIUtil.dispatchAllInvocationEvents();
|
|
}
|
|
try {
|
|
future.get();
|
|
}
|
|
catch (Exception e) {
|
|
throw new AssertionError(e);
|
|
}
|
|
|
|
waitForDaemonToStart(getProject(), document, 60_000);
|
|
backspace();
|
|
PsiDocumentManager.getInstance(getProject()).commitAllDocuments();
|
|
}
|
|
});
|
|
}
|
|
|
|
enum DEvent { STARTED, FINISHED, CANCELED }
|
|
public void testDaemonListenerEventsMustBePairedEvenWhenModalitySuddenlyChangedHalfRoad() {
|
|
configureByText(JavaFileType.INSTANCE, """
|
|
class AClass<caret> {
|
|
|
|
}
|
|
""");
|
|
Document document = getDocument(getFile());
|
|
assertEmpty(highlightErrors());
|
|
List<Pair<DEvent,String>> eventLog = Collections.synchronizedList(new ArrayList<>());
|
|
getProject().getMessageBus().connect(getTestRootDisposable()).subscribe(DaemonCodeAnalyzer.DAEMON_EVENT_TOPIC,
|
|
new DaemonCodeAnalyzer.DaemonListener() {
|
|
@Override
|
|
public void daemonStarting(@NotNull Collection<? extends @NotNull FileEditor> fileEditors) {
|
|
eventLog.add(Pair.create(DEvent.STARTED,""));
|
|
}
|
|
|
|
@Override
|
|
public void daemonFinished(@NotNull Collection<? extends @NotNull FileEditor> fileEditors) {
|
|
eventLog.add(Pair.create(DEvent.FINISHED, ""));
|
|
}
|
|
|
|
@Override
|
|
public void daemonCancelEventOccurred(@NotNull String reason) {
|
|
eventLog.add(Pair.create(DEvent.CANCELED, reason));
|
|
}
|
|
});
|
|
runWithReparseDelay(0, () -> {
|
|
for (int i=0; i<100; i++) {
|
|
waitForDaemonToFinish(getProject(), document);
|
|
eventLog.clear();
|
|
Disposable disposable = Disposer.newDisposable();
|
|
type("x");
|
|
waitForDaemonToStart(getProject(), document, 10_000);
|
|
myDaemonCodeAnalyzer.disableUpdateByTimer(disposable);
|
|
{
|
|
long deadline = System.currentTimeMillis() + 10; // do something for awhile
|
|
while (System.currentTimeMillis() < deadline) {
|
|
UIUtil.dispatchAllInvocationEvents();
|
|
}
|
|
}
|
|
|
|
type("y");
|
|
Disposer.dispose(disposable); //reenable DCA
|
|
waitForDaemonToFinish(getProject(), document);
|
|
|
|
assertEventsArePaired(eventLog);
|
|
backspace();
|
|
backspace();
|
|
}
|
|
});
|
|
}
|
|
|
|
private static void assertEventsArePaired(List<Pair<DEvent,String>> log) {
|
|
String toString = log.toString();
|
|
int starts = 0;
|
|
int ends = 0;
|
|
for (Pair<DEvent,String> e : log) {
|
|
switch(e.getFirst()) {
|
|
case STARTED:
|
|
assertEquals(toString, ends,starts);
|
|
starts++;
|
|
break;
|
|
case CANCELED:
|
|
case FINISHED:
|
|
assertTrue(toString, starts > 0);
|
|
assertTrue(toString, starts >= ends);
|
|
ends++;
|
|
break;
|
|
default:
|
|
fail(toString);
|
|
}
|
|
}
|
|
assertEquals(toString, starts, ends);
|
|
}
|
|
|
|
public void testFileOutsideProjectRootsMustNotRestartDaemonTooOften() throws ExecutionException, InterruptedException {
|
|
VirtualFile root = createVirtualDirectoryForContentFile();
|
|
VirtualFile virtualFile = createChildData(createChildDirectory(root, ".git"), "config");
|
|
|
|
String text = "[xxx]\nblah-blah";
|
|
setFileText(virtualFile, text);
|
|
|
|
assertEquals(PlainTextFileType.INSTANCE, virtualFile.getFileType());
|
|
PsiFile psiFile = getPsiManager().findFile(virtualFile);
|
|
assertNull(String.valueOf(psiFile), psiFile);
|
|
|
|
AtomicInteger restarts = new AtomicInteger();
|
|
getProject().getMessageBus().connect(getTestRootDisposable()).subscribe(DaemonCodeAnalyzer.DAEMON_EVENT_TOPIC,
|
|
new DaemonCodeAnalyzer.DaemonListener() {
|
|
@Override
|
|
public void daemonStarting(@NotNull Collection<? extends @NotNull FileEditor> fileEditors) {
|
|
restarts.incrementAndGet();
|
|
}
|
|
});
|
|
configureByExistingFile(virtualFile);
|
|
|
|
TestTimeOut t = TestTimeOut.setTimeout(10, TimeUnit.SECONDS);
|
|
while (!t.isTimedOut()) {
|
|
assertTrue(restarts.toString(), restarts.get() < 10);
|
|
UIUtil.dispatchAllInvocationEvents();
|
|
}
|
|
}
|
|
|
|
}
|