diff --git a/java/java-impl/src/META-INF/JavaPlugin.xml b/java/java-impl/src/META-INF/JavaPlugin.xml
index f107afb124df..dfc59b7fa211 100644
--- a/java/java-impl/src/META-INF/JavaPlugin.xml
+++ b/java/java-impl/src/META-INF/JavaPlugin.xml
@@ -2270,6 +2270,7 @@
+
diff --git a/java/java-impl/src/com/intellij/testIntegration/GenerateTestDataPathCommon.java b/java/java-impl/src/com/intellij/testIntegration/GenerateTestDataPathCommon.java
new file mode 100644
index 000000000000..3fa324da0db3
--- /dev/null
+++ b/java/java-impl/src/com/intellij/testIntegration/GenerateTestDataPathCommon.java
@@ -0,0 +1,109 @@
+// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.intellij.testIntegration;
+
+import com.intellij.codeInsight.AnnotationUtil;
+import com.intellij.codeInsight.CodeInsightActionHandler;
+import com.intellij.codeInsight.generation.actions.BaseGenerateAction;
+import com.intellij.find.FindBundle;
+import com.intellij.java.JavaBundle;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.roots.ProjectRootManager;
+import com.intellij.openapi.ui.DialogWrapper;
+import com.intellij.openapi.ui.ValidationInfo;
+import com.intellij.openapi.util.NlsSafe;
+import com.intellij.psi.*;
+import com.intellij.ui.components.JBPanel;
+import com.intellij.ui.components.JBTextField;
+import org.jetbrains.annotations.NonNls;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.uast.UAnnotation;
+import org.jetbrains.uast.UastContextKt;
+
+import javax.swing.*;
+import java.awt.*;
+import java.util.Collections;
+import java.util.List;
+
+class GenerateTestDataPathCommon extends BaseGenerateAction {
+
+ @NonNls
+ private static final String CONTENT_ROOT_VARIABLE = "$CONTENT_ROOT";
+ @NonNls
+ private static final String PROJECT_ROOT_VARIABLE = "$PROJECT_ROOT";
+ @NonNls
+ protected static final String ANNOTATION_FQN = "com.intellij.testFramework.TestDataPath";
+ protected static final Logger LOG = Logger.getInstance(GenerateTestDataPathCommon.class);
+
+ GenerateTestDataPathCommon(CodeInsightActionHandler handler) {
+ super(handler);
+ }
+
+ @Nullable
+ protected static String annotationValue(@NotNull PsiModifierListOwner owner, String annotationFqName) {
+ var annotationNames = Collections.singleton(annotationFqName);
+ var nestedClass = owner instanceof PsiClass && ((PsiClass)owner).getContainingClass() != null;
+ var element = nestedClass
+ ? AnnotationUtil.findAnnotation(owner, annotationNames)
+ : AnnotationUtil.findAnnotationInHierarchy(owner, annotationNames);
+ final var annotation = UastContextKt.toUElement(element, UAnnotation.class);
+ if (annotation != null) {
+ var value = annotation.findAttributeValue(PsiAnnotation.DEFAULT_REFERENCED_METHOD_NAME);
+ if (value != null) {
+ final var project = owner.getProject();
+ final var constantValue = value.evaluate();
+ if (constantValue instanceof String) {
+ var path = (String)constantValue;
+ if (path.contains(CONTENT_ROOT_VARIABLE)) {
+ final var fileIndex = ProjectRootManager.getInstance(project).getFileIndex();
+ final var file = owner.getContainingFile().getVirtualFile();
+ if (file == null) return null;
+ final var contentRoot = fileIndex.getContentRootForFile(file);
+ if (contentRoot == null) return null;
+ path = path.replace(CONTENT_ROOT_VARIABLE, contentRoot.getPath());
+ }
+ if (path.contains(PROJECT_ROOT_VARIABLE)) {
+ var baseDir = project.getBasePath();
+ if (baseDir == null) {
+ return null;
+ }
+ path = path.replace(PROJECT_ROOT_VARIABLE, baseDir);
+ }
+ return path;
+ }
+ }
+ }
+ return null;
+ }
+
+ @Override
+ protected boolean isValidForClass(PsiClass targetClass) {
+ return AnnotationUtil.findAnnotation(targetClass, "com.intellij.testFramework.TestDataPath") != null;
+ }
+
+ protected static abstract class TestDataPathDialog extends DialogWrapper {
+ protected TestDataPathDialog(@Nullable Project project, @NlsSafe @NotNull String titleText, boolean canBeParent) {
+ super(project, canBeParent);
+ setTitle(titleText);
+ setOKButtonText(JavaBundle.message("generate.button.title"));
+ init();
+ }
+
+ @Override
+ protected @Nullable JComponent createCenterPanel() {
+ var mainPanel = createMainPanel();
+ getPanelContent().forEach(mainPanel::add);
+ return mainPanel;
+ }
+
+ abstract protected List getPanelContent();
+
+ protected JBPanel createMainPanel() {
+ var mainPanel = new JBPanel<>().withPreferredWidth(250);
+ var layout = new BoxLayout(mainPanel, BoxLayout.Y_AXIS);
+ mainPanel.setLayout(layout);
+ return mainPanel;
+ }
+ }
+}
diff --git a/java/java-impl/src/com/intellij/testIntegration/GenerateTestDataPathFileAction.java b/java/java-impl/src/com/intellij/testIntegration/GenerateTestDataPathFileAction.java
new file mode 100644
index 000000000000..38a4dddfb92e
--- /dev/null
+++ b/java/java-impl/src/com/intellij/testIntegration/GenerateTestDataPathFileAction.java
@@ -0,0 +1,119 @@
+// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.intellij.testIntegration;
+
+import com.intellij.java.JavaBundle;
+import com.intellij.notification.NotificationGroupManager;
+import com.intellij.notification.NotificationType;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.command.WriteCommandAction;
+import com.intellij.openapi.ui.ValidationInfo;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFileManager;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.intellij.ui.components.*;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import java.awt.*;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class GenerateTestDataPathFileAction extends GenerateTestDataPathCommon {
+
+ public GenerateTestDataPathFileAction() {
+ super(((project, editor, file) -> {
+ var currentClass = PsiTreeUtil.findChildOfType(file, PsiClass.class);
+ if (currentClass == null) return;
+
+ var annotationValue = annotationValue(currentClass, ANNOTATION_FQN);
+ if (annotationValue == null) return;
+
+ var directoryPath = LocalFileSystem.getInstance().findFileByPath(annotationValue);
+ if (directoryPath == null || !directoryPath.isDirectory() || directoryPath.getChildren() == null) {
+ NotificationGroupManager.getInstance().getNotificationGroup("Test integration")
+ .createNotification(JavaBundle.message("generate.method.nosuites.warn", annotationValue), NotificationType.WARNING)
+ .notify(project);
+ return;
+ }
+
+ var suggestedFileNames = Arrays
+ .stream(currentClass.getAllMethods())
+ .filter(method -> method.getName().startsWith("test"))
+ .map(method -> method.getName().substring(4))
+ .collect(Collectors.toSet());
+
+ if (suggestedFileNames.isEmpty()) {
+ NotificationGroupManager.getInstance().getNotificationGroup("Test integration")
+ .createNotification(JavaBundle.message("generate.method.nofiles.warn", currentClass.getName(), annotationValue),
+ NotificationType.WARNING)
+ .notify(project);
+ return;
+ }
+
+ var dialog = new TestDataPathDialog(project, JavaBundle.message("dialog.title.testdatapath.file.generate"), false) {
+
+ private JBList suggestedFileList;
+ private JCheckBox qfCheckbox;
+ private JBTextField extensionField;
+
+ @Override
+ protected List getPanelContent() {
+ suggestedFileList = new JBList(suggestedFileNames);
+ suggestedFileList.setSelectionInterval(0, suggestedFileList.getItemsCount());
+ var scrollPane = new JBScrollPane(suggestedFileList);
+ scrollPane.setAlignmentX(Component.LEFT_ALIGNMENT);
+
+ qfCheckbox = new JCheckBox();
+ qfCheckbox.setText(JavaBundle.message("generate.quickfix.files"));
+ qfCheckbox.setAlignmentX(Component.LEFT_ALIGNMENT);
+ qfCheckbox.setSelected(false);
+
+ extensionField = new JBTextField();
+ extensionField.getEmptyText().setText(JavaBundle.message("generate.file.extension.text"));
+ extensionField.setAlignmentX(Component.LEFT_ALIGNMENT);
+
+ return List.of(scrollPane, qfCheckbox, extensionField);
+ }
+
+ @Override
+ protected void doOKAction() {
+ getSelectedMethod().forEach(fileName -> {
+ var requester = new Object();
+ var folder = VirtualFileManager.getInstance().findFileByNioPath(Path.of(annotationValue));
+ WriteCommandAction.runWriteCommandAction(project, (Runnable)() -> {
+ try {
+ folder.createChildData(requester, fileName + extensionField.getText());
+ if (isQFFileNeeded()) folder.createChildData(requester, fileName + ".after" + extensionField.getText());
+ }
+ catch (IOException | NullPointerException e) {
+ LOG.warn(e);
+ }
+ });
+ });
+ super.doOKAction();
+ }
+
+ @Override
+ protected @Nullable ValidationInfo doValidate() {
+ var in = extensionField.getText().trim();
+ if (in.isBlank() || !in.startsWith(".") || in.contains(" ")) {
+ return new ValidationInfo(JavaBundle.message("generate.file.extension.validation.error", in));
+ }
+ return null;
+ }
+
+ List getSelectedMethod() { return suggestedFileList.getSelectedValuesList(); }
+
+ boolean isQFFileNeeded() { return qfCheckbox.isSelected(); }
+ };
+
+ ApplicationManager.getApplication().invokeLater(() -> dialog.show());
+ })
+ );
+ }
+}
+
diff --git a/java/java-impl/src/com/intellij/testIntegration/GenerateTestDataPathMethodAction.java b/java/java-impl/src/com/intellij/testIntegration/GenerateTestDataPathMethodAction.java
new file mode 100644
index 000000000000..ab80da41c708
--- /dev/null
+++ b/java/java-impl/src/com/intellij/testIntegration/GenerateTestDataPathMethodAction.java
@@ -0,0 +1,106 @@
+// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.intellij.testIntegration;
+
+import com.intellij.java.JavaBundle;
+import com.intellij.notification.NotificationGroupManager;
+import com.intellij.notification.NotificationType;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.command.WriteCommandAction;
+import com.intellij.openapi.ui.ComboBox;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiElementFactory;
+import com.intellij.psi.PsiMethod;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.intellij.ui.components.JBList;
+import com.intellij.ui.components.JBScrollPane;
+import org.jetbrains.annotations.NotNull;
+
+import javax.lang.model.element.Modifier;
+import java.awt.*;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class GenerateTestDataPathMethodAction extends GenerateTestDataPathCommon {
+
+ public GenerateTestDataPathMethodAction() {
+ super(((project, editor, file) -> {
+ var currentClass = PsiTreeUtil.findChildOfType(file, PsiClass.class);
+ if (currentClass == null) return;
+
+ var existingMethodNames = Arrays
+ .stream(currentClass.getAllMethods())
+ .map(PsiMethod::getName)
+ .filter(name -> name.startsWith("test"))
+ .collect(Collectors.toCollection(HashSet::new));
+
+ var annotationValue = annotationValue(currentClass, ANNOTATION_FQN);
+ if (annotationValue == null) {
+ return;
+ }
+
+ var directoryPath = LocalFileSystem.getInstance().findFileByPath(annotationValue);
+ if (directoryPath == null || !directoryPath.isDirectory() || directoryPath.getChildren() == null) {
+ NotificationGroupManager.getInstance().getNotificationGroup("Test integration")
+ .createNotification(JavaBundle.message("generate.method.nosuites.warn", annotationValue), NotificationType.WARNING)
+ .notify(project);
+ return;
+ }
+
+ var preparedMethodNames = Arrays.stream(directoryPath.getChildren())
+ .filter(vFile -> vFile.getExtension() != null && (!vFile.getExtension().equals("java") || !vFile.getExtension().equals("kt")))
+ .map(vFile -> "test" + capitalizeFirstChar(normalizeMethodName(vFile.getName())))
+ .filter(methodName -> !existingMethodNames.contains(methodName))
+ .collect(Collectors.toSet());
+
+ var dialog = new TestDataPathDialog(project, JavaBundle.message("dialog.title.testdatapath.method.generate"), false) {
+
+ private JBList suggestedFileList;
+ private ComboBox selectModifierBox;
+
+ @Override
+ protected List getPanelContent() {
+ suggestedFileList = new JBList<>(preparedMethodNames);
+ suggestedFileList.setSelectionInterval(0, suggestedFileList.getItemsCount());
+ var scrollPane = new JBScrollPane(suggestedFileList);
+ scrollPane.setAlignmentX(Component.LEFT_ALIGNMENT);
+
+ final var choices = new String[]{JavaBundle.message("generate.select.default.modifier.text"), Modifier.PUBLIC.toString(), Modifier.PROTECTED.toString(), Modifier.PRIVATE.toString()};
+ selectModifierBox = new ComboBox(choices);
+ selectModifierBox.setAlignmentX(Component.LEFT_ALIGNMENT);
+
+ return List.of(scrollPane, selectModifierBox);
+ }
+
+ @Override
+ protected void doOKAction() {
+ suggestedFileList.getSelectedValuesList().forEach(methodName -> {
+ WriteCommandAction.runWriteCommandAction(project, (Runnable)() -> currentClass.add(
+ PsiElementFactory.getInstance(project).createMethodFromText(getSelectedModifier() + "void " + methodName + "(){ }", currentClass))
+ );
+ });
+ super.doOKAction();
+ }
+
+ String getSelectedModifier() {
+ return selectModifierBox.getSelectedIndex() == 0 ? "" : selectModifierBox.getSelectedItem().toString() + " ";
+ }
+ };
+
+ ApplicationManager.getApplication().invokeLater(() -> dialog.show());
+ })
+ );
+ }
+
+ private static String capitalizeFirstChar(@NotNull String inString) {
+ if (Character.isUpperCase(inString.charAt(0))) return inString;
+ if (inString.length() == 1) return inString.toUpperCase();
+ return inString.charAt(0) + inString.substring(1);
+ }
+
+ private static String normalizeMethodName(@NotNull String inString) {
+ return inString.substring(0, inString.indexOf("."));
+ }
+}
diff --git a/java/openapi/resources/messages/JavaBundle.properties b/java/openapi/resources/messages/JavaBundle.properties
index adb91d1f3ac5..71de41c953c1 100644
--- a/java/openapi/resources/messages/JavaBundle.properties
+++ b/java/openapi/resources/messages/JavaBundle.properties
@@ -223,6 +223,8 @@ dialog.title.infer.nullity.results=Infer Nullity Results
dialog.title.no.test.roots.found=No Test Roots Found
dialog.title.process.duplicates=Process Duplicates
dialog.title.super.method.found=Super Method Found
+dialog.title.testdatapath.method.generate=Generate methods from @TestDataPath
+dialog.title.testdatapath.file.generate=Generate files from class to @TestDataPath
do.not.import.inner.classes.for=Exclude inner classes by short name:
do.not.import.inner.classes.no.classes=No inner classes defined
editbox.blanklines.around.initializer=Around initializer:
@@ -325,6 +327,12 @@ generate.tostring.sort.super=Super class members first
generate.tostring.tab.title=toString() Generation Settings
generate.tostring.template.label=&Template:
generate.tostring.title=Generate toString()
+generate.quickfix.files=Create QF scenarios(with '.after' suffix)
+generate.select.default.modifier.text=Select modifier
+generate.file.extension.text=Enter file extension: .php, .java, .kt, .cpp ....
+generate.file.extension.validation.error=File extension ''{0}'' isn't valid. It must start from ''.'', can't contain space symbols or be blank
+generate.method.nosuites.warn=No test suites were found in @TestDataPath("{0}")
+generate.method.nofiles.warn=No test methods were found in class {0}
goto.super.class.chooser.title=Choose super class or interface
group.javadoc.alignment=Alignment
group.javadoc.blank.lines=Blank lines
@@ -1738,6 +1746,7 @@ notification.group.preview.features=Preview Java language level may be discontin
notification.group.redundant.exports=Redundant exports/opens can be removed
notification.group.setup.sdk=JDK configured
notification.group.setup.external.annotations=Failed to load external annotations
+notification.group.testintegration=Failed to generate tests for @TestDataPath
popup.title.effective.visibility=Effective Visibility
inspection.io.stream.constructor.description='InputStream' and 'OutputStream' can be constructed using 'Files' methods
inspection.input.stream.constructor.message='InputStream' can be constructed using 'Files.newInputStream'
diff --git a/platform/platform-resources-en/src/messages/ActionsBundle.properties b/platform/platform-resources-en/src/messages/ActionsBundle.properties
index 8a27ff070b11..b755c9dce1c1 100644
--- a/platform/platform-resources-en/src/messages/ActionsBundle.properties
+++ b/platform/platform-resources-en/src/messages/ActionsBundle.properties
@@ -227,6 +227,8 @@ action.GenerateBeforeClassMethod.text=BeforeClass Method
action.GenerateTearDownMethod.text=TearDown Method
action.GenerateAfterClassMethod.text=AfterClass Method
action.GenerateDataMethod.text=Parameters Method
+action.GenerateTestDataPathMethods.text=@TestDataPath Methods
+action.GenerateTestDataPathFiles.text=@TestDataPath Files
action.GenerateConstructor.text=Constructor
action.GenerateConstructor.description=Generate constructor
action.GenerateCreateUI.text=createUI(...)
diff --git a/resources/src/idea/JavaActions.xml b/resources/src/idea/JavaActions.xml
index 7325804962c6..49b06ec88b27 100644
--- a/resources/src/idea/JavaActions.xml
+++ b/resources/src/idea/JavaActions.xml
@@ -280,6 +280,8 @@
+
+