[mod-commands] Testing: declarative testing of multi-step actions

GitOrigin-RevId: d8c195494ae7973a44c1daf707fc38e6f76a7191
This commit is contained in:
Tagir Valeev
2023-07-20 18:08:24 +02:00
committed by intellij-monorepo-bot
parent be9a7b3ed9
commit 9ab2a2cd01
23 changed files with 134 additions and 104 deletions

View File

@@ -0,0 +1,16 @@
// "Access static 'AClass.fff' via class 'AClass' reference|->Delete possible side effects" "true-preview"
class AClass
{
AClass getA() {
return null;
}
static int fff;
}
class acc {
int f() {
AClass a = null;
return AClass.fff;
}
}

View File

@@ -1,4 +1,4 @@
// "Access static 'AClass.fff' via class 'AClass' reference" "true-preview"
// "Access static 'AClass.fff' via class 'AClass' reference|->Extract possible side effects" "true-preview"
class AClass
{

View File

@@ -1,4 +1,4 @@
// "Access static 'R.rr' via class 'R' reference" "true-preview"
// "Access static 'R.rr' via class 'R' reference|->Extract possible side effects" "true-preview"
class AClass
{

View File

@@ -0,0 +1,16 @@
// "Access static 'AClass.fff' via class 'AClass' reference|->Delete possible side effects" "true-preview"
class AClass
{
AClass getA() {
return null;
}
static int fff;
}
class acc {
int f() {
AClass a = null;
return <caret>a.getA().fff;
}
}

View File

@@ -1,4 +1,4 @@
// "Access static 'AClass.fff' via class 'AClass' reference" "true-preview"
// "Access static 'AClass.fff' via class 'AClass' reference|->Extract possible side effects" "true-preview"
class AClass
{

View File

@@ -1,4 +1,4 @@
// "Access static 'R.rr' via class 'R' reference" "true-preview"
// "Access static 'R.rr' via class 'R' reference|->Extract possible side effects" "true-preview"
class AClass
{

View File

@@ -1,4 +1,4 @@
// "Replace static import with qualified access to Arrays" "true-preview"
// "Replace static import with qualified access to Arrays|->Replace this occurrence and keep the import" "true-preview"
import java.util.Arrays;
import static java.util.Arrays.*;

View File

@@ -1,4 +1,4 @@
// "Replace static import with qualified access to Arrays" "true-preview"
// "Replace static import with qualified access to Arrays|->Replace all and delete the import" "true-preview"
import java.util.Arrays;
class Test {

View File

@@ -0,0 +1,9 @@
// "Replace static import with qualified access to Arrays|->Replace this occurrence and keep the import" "true-preview"
import static java.util.Arrays.*;
class Test {
public void sendMessage(String... destinationAddressNames) {
s<caret>ort(destinationAddressNames);
asList(destinationAddressNames)
}
}

View File

@@ -1,4 +1,4 @@
// "Replace static import with qualified access to Arrays" "true-preview"
// "Replace static import with qualified access to Arrays|->Replace all and delete the import" "true-preview"
import static java.util.Arrays.*;
class Test {

View File

@@ -1,4 +1,4 @@
// "Extract Set from comparison chain" "true-preview"
// "Extract Set from comparison chain|->Replace all occurrences" "true-preview"
import java.util.Collections;
import java.util.EnumSet;

View File

@@ -1,4 +1,4 @@
// "Extract Set from comparison chain" "true-preview"
// "Extract Set from comparison chain|->Replace all occurrences" "true-preview"
public class Test {
enum Status {

View File

@@ -1,35 +0,0 @@
// "Extract Set from comparison chain" "true-preview"
import java.util.Collections;
import java.util.EnumSet;
import java.util.Set;
public class Test {
private static final Set<Status> STATUSES = Collections.unmodifiableSet(EnumSet.of(Status.VALID, Status.PENDING));
enum Status {
VALID, PENDING, INVALID, UNKNOWN;
}
void test1(Status status) {
if(STATUSES.contains(status)) {
System.out.println("ok");
}
}
static class Another {
static final String STATUSES = "";
void test2(Status st) {
if(st == null || Status.PENDING == st || Status.VALID == st || Math.random() > 0.5) {
System.out.println("Replace here as well");
}
}
void test3(Status st2) {
if(st2 == Status.VALID || st2 == Status.PENDING || st2 == Status.UNKNOWN) {
System.out.println("Do not replace as we test three statuses");
}
}
}
}

View File

@@ -1,4 +1,4 @@
// "Replace with '0'" "true-preview"
// "Replace with '0'|->Extract possible side effects" "true-preview"
import java.util.stream.*;
class Test {

View File

@@ -1,4 +1,4 @@
// "Replace with '0'" "true-preview"
// "Replace with '0'|->Extract possible side effects" "true-preview"
import java.util.stream.*;
class Test {

View File

@@ -4,22 +4,9 @@ package com.intellij.codeInsight.daemon.impl.quickfix;
import com.intellij.codeInsight.daemon.quickFix.LightQuickFixParameterizedTestCase;
import com.intellij.codeInspection.LocalInspectionTool;
import com.intellij.codeInspection.accessStaticViaInstance.AccessStaticViaInstance;
import com.intellij.ui.ChooserInterceptor;
import com.intellij.ui.UiInterceptors;
import org.jetbrains.annotations.NotNull;
import java.util.List;
public class AccessStaticViaInstanceTest extends LightQuickFixParameterizedTestCase {
@Override
protected void setUp() throws Exception {
super.setUp();
if (getTestName(false).endsWith("SideEffect.java")) {
UiInterceptors.register(new ChooserInterceptor(List.of("Extract possible side effects", "Delete possible side effects"),
"Extract possible side effects"));
}
}
@Override
protected LocalInspectionTool @NotNull [] configureLocalInspectionTools() {
return new LocalInspectionTool[] {new AccessStaticViaInstance()};

View File

@@ -6,27 +6,14 @@ import com.intellij.codeInspection.LocalInspectionTool;
import com.intellij.codeInspection.dataFlow.ConstantValueInspection;
import com.intellij.testFramework.LightProjectDescriptor;
import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase;
import com.intellij.ui.ChooserInterceptor;
import com.intellij.ui.UiInterceptors;
import org.jetbrains.annotations.NotNull;
import java.util.List;
public class ReplaceWithConstantValueFixTest extends LightQuickFixParameterizedTestCase {
@Override
protected LocalInspectionTool @NotNull [] configureLocalInspectionTools() {
return new LocalInspectionTool[]{new ConstantValueInspection()};
}
@Override
protected void setUp() throws Exception {
super.setUp();
if (getTestName(false).contains("SideEffect")) {
UiInterceptors.register(new ChooserInterceptor(List.of("Extract possible side effects", "Delete possible side effects"),
"Extract possible side effects"));
}
}
@NotNull
@Override
protected LightProjectDescriptor getProjectDescriptor() {

View File

@@ -8,16 +8,6 @@ import com.intellij.ui.UiInterceptors;
import java.util.List;
public class ExpandStaticImportActionTest extends LightIntentionActionTestCase {
@Override
protected void setUp() throws Exception {
super.setUp();
if (getTestName(false).contains("Multiple")) {
UiInterceptors.register(new ChooserInterceptor(List.of("Replace this occurrence and keep the import",
"Replace all and delete the import"),
"Replace all and delete the import"));
}
}
@Override
protected String getBasePath() {
return "/codeInsight/daemonCodeAnalyzer/quickFix/expandStaticImport";

View File

@@ -23,16 +23,6 @@ import com.intellij.ui.UiInterceptors;
import java.util.List;
public class ExtractSetFromComparisonChainActionTest extends LightIntentionActionTestCase {
@Override
protected void setUp() throws Exception {
super.setUp();
if (getTestName(false).equals("Duplicate.java")) {
UiInterceptors.register(new ChooserInterceptor(List.of(
"Replace only this occurrence",
"Replace all occurrences"), "Replace all occurrences"));
}
}
@Override
protected LanguageLevel getDefaultLanguageLevel() {
return LanguageLevel.JDK_1_8;

View File

@@ -44,7 +44,8 @@ abstract class LightQuickFixParameterizedTestCase5(projectDescriptor: LightProje
fun parameterized(fileName: String) {
val filePath = "/" + LightQuickFixTestCase.BEFORE_PREFIX + fileName
val file = fixture.configureByFile(filePath)
val action = runReadAction { ActionHint.parse(file, file.text) }.findAndCheck(fixture.availableIntentions) {
val (hint, context) = runReadAction { ActionHint.parse(file, file.text) to fixture.actionContext }
val action = hint.findAndCheck(fixture.availableIntentions, context) {
"""
Test: ${getRelativePath() + filePath}
Language level: ${PsiUtil.getLanguageLevel(fixture.project)}

View File

@@ -6,6 +6,7 @@ import com.intellij.codeInsight.daemon.impl.HighlightInfo;
import com.intellij.codeInsight.daemon.impl.HighlightInfoType;
import com.intellij.codeInsight.intention.IntentionAction;
import com.intellij.codeInsight.intention.impl.preview.IntentionPreviewPopupUpdateProcessor;
import com.intellij.modcommand.ModCommandAction.ActionContext;
import com.intellij.openapi.application.ReadAction;
import com.intellij.openapi.application.impl.NonBlockingReadActionImpl;
import com.intellij.openapi.command.CommandProcessor;
@@ -92,6 +93,7 @@ public abstract class LightQuickFixTestCase extends LightDaemonAnalyzerTestCase
@NotNull String testName,
@NotNull QuickFixTestCase quickFix) throws Exception {
IntentionAction action = actionHint.findAndCheck(quickFix.getAvailableActions(),
ActionContext.from(getEditor(), getFile()),
() -> getTestInfo(testFullPath, quickFix));
if (action != null) {
String text = action.getText();

View File

@@ -1,6 +1,7 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight.daemon.quickFix;
import com.intellij.codeInsight.intention.CommonIntentionAction;
import com.intellij.codeInsight.intention.IntentionAction;
import com.intellij.codeInsight.intention.IntentionActionDelegate;
import com.intellij.codeInspection.ProblemHighlightType;
@@ -8,6 +9,10 @@ import com.intellij.codeInspection.ex.QuickFixWrapper;
import com.intellij.lang.Commenter;
import com.intellij.lang.LanguageCommenters;
import com.intellij.lang.injection.InjectedLanguageManager;
import com.intellij.modcommand.ModChooseAction;
import com.intellij.modcommand.ModCommand;
import com.intellij.modcommand.ModCommandAction;
import com.intellij.modcommand.ModCommandAction.ActionContext;
import com.intellij.psi.PsiFile;
import com.intellij.testFramework.fixtures.CodeInsightTestFixture;
import com.intellij.util.containers.ContainerUtil;
@@ -78,6 +83,8 @@ public final class ActionHint {
/**
* Finds the action which matches this ActionHint and returns it or returns null
* if this ActionHint asserts that no action should be present.
* <p>
* Use {@link #findAndCheck(Collection, ActionContext, Supplier)} if you expect multistep action
*
* @param actions actions collection to search inside
* @param infoSupplier a supplier which provides additional info which will be appended to exception message if check fails
@@ -86,13 +93,57 @@ public final class ActionHint {
*/
@Nullable
public IntentionAction findAndCheck(@NotNull Collection<? extends IntentionAction> actions, @NotNull Supplier<String> infoSupplier) {
IntentionAction result = ContainerUtil.find(actions, t -> {
String text = t.getText();
return myExactMatch ? text.equals(myExpectedText) : text.startsWith(myExpectedText);
});
return findAndCheck(actions, null, infoSupplier);
}
/**
* Finds the action which matches this ActionHint and returns it or returns null
* if this ActionHint asserts that no action should be present.
*
* @param actions actions collection to search inside
* @param context action execution context
* @param infoSupplier a supplier which provides additional info which will be appended to exception message if check fails
* @return the action or null
* @throws AssertionError if no action is found, but it should present, or if action is found, but it should not present.
*/
@Nullable
public IntentionAction findAndCheck(@NotNull Collection<? extends IntentionAction> actions,
@Nullable ActionContext context,
@NotNull Supplier<String> infoSupplier) {
String[] steps = myExpectedText.split("\\|->");
CommonIntentionAction found = null;
Collection<? extends CommonIntentionAction> commonActions = actions;
for (int i = 0; i < steps.length; i++) {
String curStep = steps[i];
found = ContainerUtil.find(commonActions, t -> {
String text = getActionText(context, t);
return myExactMatch ? text.equals(curStep) : text.startsWith(curStep);
});
if (i == steps.length - 1) break;
if (context == null) {
fail("Action context is not supplied");
}
if (found == null) {
fail(exceptionHeader(curStep) + " not found\nAvailable actions: " +
commonActions.stream().map(act -> getActionText(context, act)).collect(Collectors.joining(", ", "[", "]\n")) +
infoSupplier.get());
}
ModCommandAction action = found.asModCommandAction();
if (action == null) {
fail(exceptionHeader(curStep) + " is not ModCommandAction");
}
ModCommand command = action.perform(context);
if (!(command instanceof ModChooseAction chooseAction)) {
fail(exceptionHeader(curStep) + " does not produce a chooser");
return null;
}
commonActions = chooseAction.actions();
}
IntentionAction result = found == null ? null : found.asIntention();
String lastStep = steps[steps.length - 1];
if(myShouldPresent) {
if(result == null) {
fail(exceptionHeader() + " not found\nAvailable actions: " +
fail(exceptionHeader(lastStep) + " not found\nAvailable actions: " +
actions.stream().map(IntentionAction::getText).collect(Collectors.joining(", ", "[", "]\n")) +
infoSupplier.get());
}
@@ -100,25 +151,39 @@ public final class ActionHint {
result = IntentionActionDelegate.unwrap(result);
ProblemHighlightType actualType = QuickFixWrapper.getHighlightType(result);
if (actualType == null) {
fail(exceptionHeader() + " is not a LocalQuickFix, but " + result.getClass().getName() +
fail(exceptionHeader(lastStep) + " is not a LocalQuickFix, but " + result.getClass().getName() +
"\nExpected LocalQuickFix with ProblemHighlightType=" + myHighlightType + "\n" +
infoSupplier.get());
}
if(actualType != myHighlightType) {
fail(exceptionHeader() + " has wrong ProblemHighlightType.\nExpected: " + myHighlightType +
fail(exceptionHeader(lastStep) + " has wrong ProblemHighlightType.\nExpected: " + myHighlightType +
"\nActual: " + actualType + "\n" + infoSupplier.get());
}
}
}
else if(result != null) {
fail(exceptionHeader() + " is present, but should not\n" + infoSupplier.get());
fail(exceptionHeader(lastStep) + " is present, but should not\n" + infoSupplier.get());
}
return result;
}
private static @NotNull String getActionText(@Nullable ActionContext context, CommonIntentionAction t) {
if (t instanceof IntentionAction intention) {
return intention.getText();
}
if (!(t instanceof ModCommandAction action)) {
throw new AssertionError("Action is not ModCommandAction: " + t);
}
if (context == null) {
fail("Context is not specified for ModCommandAction");
}
ModCommandAction.Presentation presentation = action.getPresentation(context);
return presentation == null ? "(unavailable) " + action.getFamilyName() : presentation.name();
}
@NotNull
private String exceptionHeader() {
return "Action with " + (myExactMatch ? "text" : "prefix") + " '" + myExpectedText + "'";
private String exceptionHeader(String text) {
return "Action with " + (myExactMatch ? "text" : "prefix") + " '" + text + "'";
}
@NotNull
@@ -131,7 +196,7 @@ public final class ActionHint {
* <p>
* Currently the following syntax is supported:
* </p>
* {@code // "quick-fix name or intention text" "true|false|<ProblemHighlightType>"}
* {@code // "quick-fix name or intention text[|->next step]" "true|false|<ProblemHighlightType>[-preview]"}
* <p>
* (replace // with line comment prefix in the corresponding language if necessary).
* If {@link ProblemHighlightType} enum value is specified instead of true/false

View File

@@ -43,6 +43,7 @@ import com.intellij.usageView.UsageInfo;
import com.intellij.usages.Usage;
import com.intellij.usages.UsageTarget;
import com.intellij.util.Consumer;
import com.intellij.util.concurrency.annotations.RequiresReadLock;
import org.intellij.lang.annotations.Language;
import org.intellij.lang.annotations.MagicConstant;
import org.jetbrains.annotations.ApiStatus.Experimental;
@@ -86,6 +87,7 @@ public interface CodeInsightTestFixture extends IdeaProjectTestFixture {
/**
* @return the action context for current in-memory editor
*/
@RequiresReadLock
default ModCommandAction.ActionContext getActionContext() {
return ModCommandAction.ActionContext.from(getEditor(), getFile());
}