diff --git a/CHANGELOG.md b/CHANGELOG.md index 14db6fcb..5aecafe6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ ## [Unreleased] ### Added -NA +N/A ### Changed - remove the annoying inspector for unittest check during writing the unittest, reason: inspector is for something that can be fast fixed, but the unittest check is not, so remove it to avoid the confusion. @@ -111,4 +111,10 @@ NA ## 1.0.14 - 2025-06-08 - remove the annoying inspector for unittest check during writing the unittest, reason: inspector is for something that can be fast fixed, but the unittest check is not, so remove it to avoid the confusion. -- add the support to Junit 5 from version 5.7.0 to 5.13.0. \ No newline at end of file +- add the support to Junit 5 from version 5.7.0 to 5.13.0. + +## 1.0.15 - 2025-06-24 +- add the support to sort the test dependencies by the order of the dependency directories. (with this for large application, user can specify which set of dependency directories to be loaded first) +- add the support to sort the test dependencies by the order of the first-load dependent jars. (with this when there are same class accross multiple jars, the class in the first-load dependent jars will be loaded first) +- align the resource directory ordering in the classpath and cp arguments with the test case file's source root. +- add the support to configure the default mutator group, dependency directories order, and first-load dependent jars. \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 44970d35..0081da73 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,7 @@ import org.jetbrains.changelog.Changelog import org.jetbrains.changelog.markdownToHTML import org.jetbrains.intellij.platform.gradle.TestFrameworkType +import org.jetbrains.intellij.platform.gradle.tasks.RunIdeTask plugins { id("java") // Java support @@ -160,10 +161,22 @@ tasks { publishPlugin { dependsOn(patchChangelog) } + } intellijPlatformTesting { runIde { + register("runIdeEn") { + task { + jvmArgumentProviders += CommandLineArgumentProvider { + listOf( + "-Duser.language=en", + "-Duser.country=US", + ) + } + } + } + register("runIdeForUiTests") { task { jvmArgumentProviders += CommandLineArgumentProvider { @@ -181,4 +194,5 @@ intellijPlatformTesting { } } } -} \ No newline at end of file +} + diff --git a/gradle.properties b/gradle.properties index df1e6eab..71e592c1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ pluginName = TestCraft-Pro pluginRepositoryUrl = https://github.com/jaksonlin/testcraft-pro # SemVer format -> https://semver.org -pluginVersion = 1.0.14 +pluginVersion = 1.0.15 # Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html diff --git a/screenshots/menu/right-click.png b/screenshots/menu/right-click.png new file mode 100644 index 00000000..86e2e460 Binary files /dev/null and b/screenshots/menu/right-click.png differ diff --git a/screenshots/menu/scan.png b/screenshots/menu/scan.png new file mode 100644 index 00000000..622e3a5a Binary files /dev/null and b/screenshots/menu/scan.png differ diff --git a/screenshots/mutation/classpath.png b/screenshots/mutation/classpath.png new file mode 100644 index 00000000..193d5a93 Binary files /dev/null and b/screenshots/mutation/classpath.png differ diff --git a/screenshots/mutation/history.png b/screenshots/mutation/history.png new file mode 100644 index 00000000..111b4356 Binary files /dev/null and b/screenshots/mutation/history.png differ diff --git a/screenshots/mutation/input-class.png b/screenshots/mutation/input-class.png new file mode 100644 index 00000000..7edbb233 Binary files /dev/null and b/screenshots/mutation/input-class.png differ diff --git a/screenshots/mutation/result-dialog.png b/screenshots/mutation/result-dialog.png new file mode 100644 index 00000000..714930df Binary files /dev/null and b/screenshots/mutation/result-dialog.png differ diff --git a/screenshots/mutation/result.png b/screenshots/mutation/result.png new file mode 100644 index 00000000..ea11e157 Binary files /dev/null and b/screenshots/mutation/result.png differ diff --git a/screenshots/mutation/select-mutate-target.jpg b/screenshots/mutation/select-mutate-target.jpg new file mode 100644 index 00000000..89f196db Binary files /dev/null and b/screenshots/mutation/select-mutate-target.jpg differ diff --git a/screenshots/mutation/select-mutate-target.png b/screenshots/mutation/select-mutate-target.png new file mode 100644 index 00000000..6d7e70af Binary files /dev/null and b/screenshots/mutation/select-mutate-target.png differ diff --git a/src/main/java/com/github/jaksonlin/testcraft/application/settings/MutationSettingsConfigurable.java b/src/main/java/com/github/jaksonlin/testcraft/application/settings/MutationSettingsConfigurable.java index 4b73fa06..6449d038 100644 --- a/src/main/java/com/github/jaksonlin/testcraft/application/settings/MutationSettingsConfigurable.java +++ b/src/main/java/com/github/jaksonlin/testcraft/application/settings/MutationSettingsConfigurable.java @@ -29,16 +29,22 @@ public JComponent createComponent() { @Override public boolean isModified() { - return !mutationSettingsComponent.getSelectedMutatorGroup().equals(MutationConfigService.getInstance().getMutatorGroup()); + return !mutationSettingsComponent.getSelectedMutatorGroup().equals(MutationConfigService.getInstance().getMutatorGroup()) + || !mutationSettingsComponent.getDependencyDirectoriesOrder().equals(MutationConfigService.getInstance().getDependencyDirectoriesOrder()) + || !mutationSettingsComponent.getFirstLoadDependentJars().equals(MutationConfigService.getInstance().getFirstLoadDependentJars()); } @Override public void apply() throws ConfigurationException { MutationConfigService.getInstance().setMutatorGroup(mutationSettingsComponent.getSelectedMutatorGroup()); + MutationConfigService.getInstance().setDependencyDirectoriesOrder(mutationSettingsComponent.getDependencyDirectoriesOrder()); + MutationConfigService.getInstance().setFirstLoadDependentJars(mutationSettingsComponent.getFirstLoadDependentJars()); } @Override public void reset() { mutationSettingsComponent.setSelectedMutatorGroup(MutationConfigService.getInstance().getMutatorGroup()); + mutationSettingsComponent.setDependencyDirectoriesOrder(MutationConfigService.getInstance().getDependencyDirectoriesOrder()); + mutationSettingsComponent.setFirstLoadDependentJars(MutationConfigService.getInstance().getFirstLoadDependentJars()); } } \ No newline at end of file diff --git a/src/main/java/com/github/jaksonlin/testcraft/infrastructure/commands/pitest/PrepareEnvironmentCommand.java b/src/main/java/com/github/jaksonlin/testcraft/infrastructure/commands/pitest/PrepareEnvironmentCommand.java index e40ecda3..e123b672 100644 --- a/src/main/java/com/github/jaksonlin/testcraft/infrastructure/commands/pitest/PrepareEnvironmentCommand.java +++ b/src/main/java/com/github/jaksonlin/testcraft/infrastructure/commands/pitest/PrepareEnvironmentCommand.java @@ -296,18 +296,74 @@ private List getPitestJunit5PluginFile(String junitPlatformLauncherJar) return junit5PitestPluginJars; } + private boolean matchesAnyPattern(String fileName, List patterns) { + for (String pattern : patterns) { + // Convert wildcard to regex + if (fileName.matches(pattern)) { + return true; + } + } + return false; + } + private void sortTestDependencies(List testDependencies) { + // sort the test dependencies by the order of the dependencies + String dependencyDirectoriesOrder = MutationConfigService.getInstance().getDependencyDirectoriesOrder(); + String[] dependencyDirectories = dependencyDirectoriesOrder.split(";"); + List firstLoadDependentJarsPatterns = MutationConfigService.getInstance().getFirstLoadDependentJarsPatterns(); + + List sortedDependencies = new ArrayList<>(); + List remainingDependencies = new ArrayList<>(testDependencies); + + // 1. Add first-load dependencies + for (String dependency : new ArrayList<>(remainingDependencies)) { + String fileName = new File(dependency).getName(); + if (matchesAnyPattern(fileName, firstLoadDependentJarsPatterns)) { + sortedDependencies.add(dependency); + remainingDependencies.remove(dependency); + } + } + + // 2. Add dependencies in the order of the dependency directories + for (String dependencyDirectory : dependencyDirectories) { + for (String dependency : new ArrayList<>(remainingDependencies)) { + String dirName = new File(dependency).getParent(); + if (dirName != null && dirName.endsWith(dependencyDirectory)) { + // Check if the dependency is in the current directory + // If so, add it to the sorted dependencies + // and remove it from the remaining dependencies + sortedDependencies.add(dependency); + remainingDependencies.remove(dependency); + } + } + } + // 3. Add remaining dependencies + sortedDependencies.addAll(remainingDependencies); + // 4. update the test dependencies + testDependencies.clear(); + testDependencies.addAll(sortedDependencies); + } + private void collectClassPathFileForPitest(String reportDirectory, String targetPackageName, List resourceDirectories) { String classPathFileContent = ReadAction.compute(() -> { + // class file output path List classpath = GradleUtils.getCompilationOutputPaths(getProject()); + // external jars List testDependencies = GradleUtils.getTestRunDependencies(getProject()); + // sort them by the orders + sortTestDependencies(testDependencies); + // 0. add the class file output path List allDependencies = new ArrayList<>(classpath); + // 1. add the resource directories if (resourceDirectories != null) { allDependencies.addAll(resourceDirectories); } + // 2. add the external jars allDependencies.addAll(testDependencies); + // 3. add the junit 5 pitest plugin jars if (getContext().getIsJunit5()) { allDependencies.addAll(getJunit5PitestPluginJars(testDependencies)); } + // 4. add the tools.jar for JDK 8 if (getContext().getJavaVersion().contains("1.8.")) { allDependencies.add(findToolsJarForJDK8()); } diff --git a/src/main/java/com/github/jaksonlin/testcraft/infrastructure/services/config/MutationConfigService.java b/src/main/java/com/github/jaksonlin/testcraft/infrastructure/services/config/MutationConfigService.java index 75a3dee7..73d7fc38 100644 --- a/src/main/java/com/github/jaksonlin/testcraft/infrastructure/services/config/MutationConfigService.java +++ b/src/main/java/com/github/jaksonlin/testcraft/infrastructure/services/config/MutationConfigService.java @@ -7,6 +7,9 @@ import org.jetbrains.annotations.Nullable; import com.intellij.openapi.components.Service; import com.intellij.openapi.application.ApplicationManager; + +import java.util.ArrayList; +import java.util.List; import java.util.Objects; @Service(Service.Level.APP) @@ -23,6 +26,8 @@ public static MutationConfigService getInstance() { public static class State { public String mutatorGroup = "STARTER_KIT"; // Default value + public String dependencyDirectoriesOrder = "bos;biz;trd"; // The order of the dependency directories + public String firstLoadDependentJars = "mockito-*.jar"; // The first load dependent jars public State() { } @@ -37,7 +42,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(mutatorGroup); + return Objects.hash(mutatorGroup, dependencyDirectoriesOrder, firstLoadDependentJars); } } @@ -51,6 +56,33 @@ public void setMutatorGroup(String mutatorGroup) { myState.mutatorGroup = mutatorGroup; } + public String getDependencyDirectoriesOrder() { + return myState.dependencyDirectoriesOrder; + } + + public void setDependencyDirectoriesOrder(String dependencyDirectoriesOrder) { + myState.dependencyDirectoriesOrder = dependencyDirectoriesOrder; + } + + public String getFirstLoadDependentJars() { + return myState.firstLoadDependentJars; + } + + public List getFirstLoadDependentJarsPatterns() { + List firstLoadDependentJarsArray = new ArrayList<>(); + String[] patterns = myState.firstLoadDependentJars.split(";"); + for (String pattern : patterns) { + // Convert wildcard to regex + String regex = pattern.replace(".", "\\.").replace("*", ".*"); + firstLoadDependentJarsArray.add(regex); + } + return firstLoadDependentJarsArray; + } + + public void setFirstLoadDependentJars(String firstLoadDependentJars) { + myState.firstLoadDependentJars = firstLoadDependentJars; + } + @Nullable @Override public State getState() { diff --git a/src/main/java/com/github/jaksonlin/testcraft/presentation/components/configuration/MutationSettingsComponent.java b/src/main/java/com/github/jaksonlin/testcraft/presentation/components/configuration/MutationSettingsComponent.java index 43d19a2c..6ac6bf6f 100644 --- a/src/main/java/com/github/jaksonlin/testcraft/presentation/components/configuration/MutationSettingsComponent.java +++ b/src/main/java/com/github/jaksonlin/testcraft/presentation/components/configuration/MutationSettingsComponent.java @@ -1,8 +1,12 @@ package com.github.jaksonlin.testcraft.presentation.components.configuration; import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; import com.github.jaksonlin.testcraft.infrastructure.services.config.MutationConfigService; +import com.github.jaksonlin.testcraft.infrastructure.services.system.I18nService; import com.intellij.openapi.ui.ComboBox; import com.intellij.ui.components.JBLabel; import com.intellij.util.ui.FormBuilder; @@ -11,15 +15,75 @@ public class MutationSettingsComponent { private final JPanel mainPanel; private final ComboBox mutatorGroupComboBox; + // Dependency Directories UI + private final DefaultListModel dependencyDirsModel = new DefaultListModel<>(); + private final JList dependencyDirsList = new JList<>(dependencyDirsModel); + + // First-Load JAR Patterns UI + private final DefaultListModel firstLoadJarsModel = new DefaultListModel<>(); + private final JList firstLoadJarsList = new JList<>(firstLoadJarsModel); + public MutationSettingsComponent() { mutatorGroupComboBox = new ComboBox<>(new String[]{"DEFAULTS", "STRONGER", "STARTER_KIT"}); mutatorGroupComboBox.setSelectedItem(MutationConfigService.getInstance().getMutatorGroup()); + // Load initial values + for (String dir : MutationConfigService.getInstance().getDependencyDirectoriesOrder().split(";")) { + if (!dir.trim().isEmpty()) dependencyDirsModel.addElement(dir.trim()); + } + for (String pattern : MutationConfigService.getInstance().getFirstLoadDependentJars().split(";")) { + if (!pattern.trim().isEmpty()) firstLoadJarsModel.addElement(pattern.trim()); + } + + // Dependency Directories Panel + JPanel depDirsPanel = createEditableListPanel( + I18nService.getInstance().message("settings.mutation.dependency.directories.order.label"), + dependencyDirsList, dependencyDirsModel); + + // First-Load JAR Patterns Panel + JPanel firstLoadJarsPanel = createEditableListPanel( + I18nService.getInstance().message("settings.mutation.first.load.jar.patterns.label"), + firstLoadJarsList, firstLoadJarsModel); + mainPanel = FormBuilder.createFormBuilder() - .addLabeledComponent(new JBLabel("Default Mutator Group:"), mutatorGroupComboBox) + .addLabeledComponent(new JBLabel(I18nService.getInstance().message("settings.mutation.default.mutator.group.label")), mutatorGroupComboBox) + .addComponent(depDirsPanel) + .addComponent(firstLoadJarsPanel) .addComponentFillVertically(new JPanel(), 0) .getPanel(); + } + + private JPanel createEditableListPanel(String label, JList list, DefaultListModel model) { + JPanel panel = new JPanel(new BorderLayout()); + panel.add(new JBLabel(label), BorderLayout.NORTH); + + JScrollPane scrollPane = new JScrollPane(list); + panel.add(scrollPane, BorderLayout.CENTER); + + JPanel buttonsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); + JButton addButton = new JButton(I18nService.getInstance().message("settings.mutation.button.add")); + JButton removeButton = new JButton(I18nService.getInstance().message("settings.mutation.button.remove")); + + addButton.addActionListener(e -> { + String input = JOptionPane.showInputDialog(panel, I18nService.getInstance().message("settings.mutation.dialog.enter.value")); + if (input != null && !input.trim().isEmpty()) { + model.addElement(input.trim()); + } + }); + removeButton.addActionListener(e -> { + int selected = list.getSelectedIndex(); + if (selected != -1) { + model.remove(selected); + } + }); + + buttonsPanel.add(addButton); + buttonsPanel.add(removeButton); + panel.add(buttonsPanel, BorderLayout.SOUTH); + + panel.setBorder(BorderFactory.createEmptyBorder(5, 0, 5, 0)); + return panel; } public JPanel getPanel() { @@ -34,4 +98,37 @@ public void setSelectedMutatorGroup(String mutatorGroup) { mutatorGroupComboBox.setSelectedItem(mutatorGroup); } + // New: Getters for the dependency settings + public String getDependencyDirectoriesOrder() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < dependencyDirsModel.size(); i++) { + if (i > 0) sb.append(";"); + sb.append(dependencyDirsModel.get(i)); + } + return sb.toString(); + } + + public String getFirstLoadDependentJars() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < firstLoadJarsModel.size(); i++) { + if (i > 0) sb.append(";"); + sb.append(firstLoadJarsModel.get(i)); + } + return sb.toString(); + } + + // New: Setters for loading from config if needed + public void setDependencyDirectoriesOrder(String dirs) { + dependencyDirsModel.clear(); + for (String dir : dirs.split(";")) { + if (!dir.trim().isEmpty()) dependencyDirsModel.addElement(dir.trim()); + } + } + + public void setFirstLoadDependentJars(String patterns) { + firstLoadJarsModel.clear(); + for (String pattern : patterns.split(";")) { + if (!pattern.trim().isEmpty()) firstLoadJarsModel.addElement(pattern.trim()); + } + } } \ No newline at end of file diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index c3f4b6c8..5a36cddd 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -31,6 +31,13 @@ Change Notes +

1.0.15 - 2025-06-24

+
    +
  • add the support to sort the test dependencies by the order of the dependency directories. (with this for large application, user can specify which set of dependency directories to be loaded first)
  • +
  • add the support to sort the test dependencies by the order of the first-load dependent jars. (with this when there are same class accross multiple jars, the class in the first-load dependent jars will be loaded first)
  • +
  • align the resource directory ordering in the classpath and cp arguments with the test case file's source root.
  • +
  • add the support to configure the default mutator group, dependency directories order, and first-load dependent jars.
  • +

1.0.14 - 2025-06-08

  • remove the annoying inspector for unittest check during writing the unittest, reason: inspector is for something that can be fast fixed, but the unittest check is not, so remove it to avoid the confusion.
  • @@ -227,7 +234,7 @@ - + diff --git a/src/main/resources/messages/MyBundle.properties b/src/main/resources/messages/MyBundle.properties index 95cb20c2..1a425736 100644 --- a/src/main/resources/messages/MyBundle.properties +++ b/src/main/resources/messages/MyBundle.properties @@ -76,6 +76,14 @@ settings.invalidTestCase.assertions.examples.2=• assertEquals(1, 1) - comparin settings.invalidTestCase.assertions.examples.3=• assertNotNull(new Object()) - testing newly created object settings.invalidTestCase.assertions.examples.4=• assertEquals("success", "success") - comparing identical strings +# Mutation Settings +settings.mutation.default.mutator.group.label=Default Mutator Group: +settings.mutation.dependency.directories.order.label=Dependency Directories Order: +settings.mutation.first.load.jar.patterns.label=First-Load JAR Patterns: +settings.mutation.button.add=Add +settings.mutation.button.remove=Remove +settings.mutation.dialog.enter.value=Enter value: + # Tool Windows toolwindow.mutation.title=TestCraft Mutation Test History toolwindow.llm.title=TestCraft LLM Suggestions @@ -168,4 +176,5 @@ testscan.found_invalid_test_cases=Found {0} invalid test cases: testscan.test_case_validation_results=Test Case Validation Results testscan.test_case_validation_canceled=Test case validation was canceled. testscan.commit_blocked_message=Commit blocked: Invalid test cases found. Please fix the issues before committing. -testscan.validation_timeout_message=Test case validation timed out. Please try again. \ No newline at end of file +testscan.validation_timeout_message=Test case validation timed out. Please try again. + diff --git a/src/main/resources/messages/MyBundle_en_US.properties b/src/main/resources/messages/MyBundle_en_US.properties index aab10e8c..f659b84a 100644 --- a/src/main/resources/messages/MyBundle_en_US.properties +++ b/src/main/resources/messages/MyBundle_en_US.properties @@ -78,6 +78,14 @@ settings.invalidTestCase.assertions.examples.2=• assertEquals(1, 1) - comparin settings.invalidTestCase.assertions.examples.3=• assertNotNull(new Object()) - testing newly created object settings.invalidTestCase.assertions.examples.4=• assertEquals("success", "success") - comparing identical strings +# Mutation Settings +settings.mutation.default.mutator.group.label=Default Mutator Group: +settings.mutation.dependency.directories.order.label=Dependency Directories Order: +settings.mutation.first.load.jar.patterns.label=First-Load JAR Patterns: +settings.mutation.button.add=Add +settings.mutation.button.remove=Remove +settings.mutation.dialog.enter.value=Enter value: + # Tool Windows toolwindow.mutation.title=TestCraft Mutation Test History toolwindow.llm.title=TestCraft LLM Suggestions diff --git a/src/main/resources/messages/MyBundle_zh_CN.properties b/src/main/resources/messages/MyBundle_zh_CN.properties index 702b8d7d..83b50124 100644 --- a/src/main/resources/messages/MyBundle_zh_CN.properties +++ b/src/main/resources/messages/MyBundle_zh_CN.properties @@ -62,6 +62,15 @@ settings.testcraft.llm.description=配置 Ollama LLM 集成以获取测试建议 settings.testcraft.mutation=变异测试 settings.testcraft.mutation.description=配置变异测试参数和变异组 settings.testcraft.mutation.mutatorGroup=变异组 + +# Mutation Settings +settings.mutation.default.mutator.group.label=默认变异组: +settings.mutation.dependency.directories.order.label=依赖目录顺序: +settings.mutation.first.load.jar.patterns.label=优先加载 JAR 包: +settings.mutation.button.add=添加 +settings.mutation.button.remove=删除 +settings.mutation.dialog.enter.value=输入值: + # Menu Actions menu.unittest.top=单元测试助手 menu.unittest.tools=单元测试辅助工具