diff --git a/bundles/org.pitest.pitclipse.ui/icons/killed.gif b/bundles/org.pitest.pitclipse.ui/icons/killed.gif new file mode 100644 index 00000000..7b081f57 Binary files /dev/null and b/bundles/org.pitest.pitclipse.ui/icons/killed.gif differ diff --git a/bundles/org.pitest.pitclipse.ui/icons/noCoverage.gif b/bundles/org.pitest.pitclipse.ui/icons/noCoverage.gif new file mode 100644 index 00000000..816e12fc Binary files /dev/null and b/bundles/org.pitest.pitclipse.ui/icons/noCoverage.gif differ diff --git a/bundles/org.pitest.pitclipse.ui/icons/nonViable.gif b/bundles/org.pitest.pitclipse.ui/icons/nonViable.gif new file mode 100644 index 00000000..d05b83e0 Binary files /dev/null and b/bundles/org.pitest.pitclipse.ui/icons/nonViable.gif differ diff --git a/bundles/org.pitest.pitclipse.ui/icons/survived.gif b/bundles/org.pitest.pitclipse.ui/icons/survived.gif new file mode 100644 index 00000000..b14446ad Binary files /dev/null and b/bundles/org.pitest.pitclipse.ui/icons/survived.gif differ diff --git a/bundles/org.pitest.pitclipse.ui/icons/timeout.gif b/bundles/org.pitest.pitclipse.ui/icons/timeout.gif new file mode 100644 index 00000000..98a30154 Binary files /dev/null and b/bundles/org.pitest.pitclipse.ui/icons/timeout.gif differ diff --git a/bundles/org.pitest.pitclipse.ui/plugin.xml b/bundles/org.pitest.pitclipse.ui/plugin.xml index d52728fe..413dc8e6 100644 --- a/bundles/org.pitest.pitclipse.ui/plugin.xml +++ b/bundles/org.pitest.pitclipse.ui/plugin.xml @@ -4,8 +4,8 @@ - + @@ -36,5 +36,250 @@ point="org.pitest.pitclipse.core.mutations.results"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.pitest.pitclipse.ui/src/org/pitest/pitclipse/ui/core/PitUiActivator.java b/bundles/org.pitest.pitclipse.ui/src/org/pitest/pitclipse/ui/core/PitUiActivator.java index ef7e44d6..901f4be1 100644 --- a/bundles/org.pitest.pitclipse.ui/src/org/pitest/pitclipse/ui/core/PitUiActivator.java +++ b/bundles/org.pitest.pitclipse.ui/src/org/pitest/pitclipse/ui/core/PitUiActivator.java @@ -28,6 +28,7 @@ import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; import org.osgi.framework.FrameworkUtil; +import org.pitest.pitclipse.ui.mutation.marker.PitclipseMutantMarkerFactory; /** * The ui activator class which initializes the icons of the plug in @@ -41,7 +42,10 @@ public class PitUiActivator extends AbstractUIPlugin { * Key under which the pit icon is put in the registry */ private static final String PIT_ICON = "org.pitest.pitclipse.pitIcon"; - + /** + * Qualifier of this plug in + */ + public static final String PLUGIN_ID = "org.pitest.pitclipse.ui"; /** * The shared instance */ @@ -61,8 +65,29 @@ private void initIcons(BundleContext context) { Display.getDefault().syncExec(() -> { imageRegistry = new ImageRegistry(); Bundle bundle = FrameworkUtil.getBundle(getClass()); - URL url = FileLocator.find(bundle, new Path("icons/pit.gif"), null); + final URL url = FileLocator.find(bundle, new Path("icons/pit.gif"), null); imageRegistry.put(PIT_ICON, ImageDescriptor.createFromURL(url).createImage()); + // annotation icons + final URL killedUrl = FileLocator.find(bundle, new Path("icons/killed.gif"), null); + imageRegistry.put(PitclipseMutantMarkerFactory.KILLED_MUTANT_MARKER, + ImageDescriptor.createFromURL(killedUrl).createImage()); + final URL survivingUrl = FileLocator.find(bundle, new Path("icons/survived.gif"), null); + imageRegistry.put(PitclipseMutantMarkerFactory.SURVIVING_MUTANT_MARKER, + ImageDescriptor.createFromURL(survivingUrl).createImage()); + final URL noCoverageUrl = FileLocator.find(bundle, new Path("icons/noCoverage.gif"), null); + imageRegistry.put(PitclipseMutantMarkerFactory.NO_COVERAGE_MUTANT_MARKER, + ImageDescriptor.createFromURL(noCoverageUrl).createImage()); + final URL timeoutUrl = FileLocator.find(bundle, new Path("icons/timeout.gif"), null); + imageRegistry.put(PitclipseMutantMarkerFactory.TIMEOUT_MUTANT_MARKER, + ImageDescriptor.createFromURL(timeoutUrl).createImage()); + // use same icon for these 3, because they are not from huge interest + final URL nonViableUrl = FileLocator.find(bundle, new Path("icons/nonViable.gif"), null); + imageRegistry.put(PitclipseMutantMarkerFactory.NON_VIABLE_MUTANT_MARKER, + ImageDescriptor.createFromURL(nonViableUrl).createImage()); + imageRegistry.put(PitclipseMutantMarkerFactory.MEMORY_ERROR_MARKER, + ImageDescriptor.createFromURL(nonViableUrl).createImage()); + imageRegistry.put(PitclipseMutantMarkerFactory.RUN_ERROR_MARKER, + ImageDescriptor.createFromURL(nonViableUrl).createImage()); }); } @@ -73,10 +98,22 @@ public Image getPitIcon() { return imageRegistry.get(PIT_ICON); } + /** + * @param imageDescritporId + * @return the requested image descriptor or null, if not found + */ + public ImageDescriptor getImageDescriptor(String imageDescritporId) { + return imageRegistry.getDescriptor(imageDescritporId); + } + /** * @return the shared instance */ public static PitUiActivator getDefault() { return plugin; } + + public Image getImage(String imageDescriptorId) { + return imageRegistry.get(imageDescriptorId); + } } diff --git a/bundles/org.pitest.pitclipse.ui/src/org/pitest/pitclipse/ui/mutation/marker/AnnotationImageProvider.java b/bundles/org.pitest.pitclipse.ui/src/org/pitest/pitclipse/ui/mutation/marker/AnnotationImageProvider.java new file mode 100644 index 00000000..a9061a69 --- /dev/null +++ b/bundles/org.pitest.pitclipse.ui/src/org/pitest/pitclipse/ui/mutation/marker/AnnotationImageProvider.java @@ -0,0 +1,44 @@ +/******************************************************************************* + * Copyright 2021 Jonas Kutscha and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + ******************************************************************************/ + +package org.pitest.pitclipse.ui.mutation.marker; + +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.text.source.Annotation; +import org.eclipse.swt.graphics.Image; +import org.eclipse.ui.texteditor.IAnnotationImageProvider; +import org.pitest.pitclipse.ui.core.PitUiActivator; + +/** + * Simple image provider for the mutation marker + * @author Jonas Kutscha + */ +public class AnnotationImageProvider implements IAnnotationImageProvider { + @Override + public String getImageDescriptorId(Annotation annotation) { + return annotation.getType(); + } + + @Override + public ImageDescriptor getImageDescriptor(String imageDescritporId) { + return PitUiActivator.getDefault().getImageDescriptor(imageDescritporId); + } + + @Override + public Image getManagedImage(Annotation annotation) { + return PitUiActivator.getDefault().getImage(getImageDescriptorId(annotation)); + } +} diff --git a/bundles/org.pitest.pitclipse.ui/src/org/pitest/pitclipse/ui/mutation/marker/PitclipseMutantMarkerFactory.java b/bundles/org.pitest.pitclipse.ui/src/org/pitest/pitclipse/ui/mutation/marker/PitclipseMutantMarkerFactory.java new file mode 100644 index 00000000..4ac00ada --- /dev/null +++ b/bundles/org.pitest.pitclipse.ui/src/org/pitest/pitclipse/ui/mutation/marker/PitclipseMutantMarkerFactory.java @@ -0,0 +1,274 @@ +/******************************************************************************* + * Copyright 2021 Jonas Kutscha and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + ******************************************************************************/ + +package org.pitest.pitclipse.ui.mutation.marker; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IType; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; +import org.pitest.pitclipse.core.extension.point.ResultNotifier; +import org.pitest.pitclipse.runner.model.ClassMutations; +import org.pitest.pitclipse.runner.model.Mutation; +import org.pitest.pitclipse.runner.model.MutationsModel; +import org.pitest.pitclipse.runner.model.PackageMutations; +import org.pitest.pitclipse.runner.model.ProjectMutations; +import org.pitest.pitclipse.runner.model.Status; +import org.pitest.pitclipse.ui.core.PitUiActivator; + +/** + * Class which creates mutation markers after a PIT run + * @author Jonas Kutscha + */ +public class PitclipseMutantMarkerFactory implements ResultNotifier { + /** + * Id where all Pitclipse markers can be found + */ + public static final String PITCLIPSE_MUTANT_MARKER = PitUiActivator.PLUGIN_ID + ".pitclipsemarker"; + /** + * Id for markers of surviving mutants + */ + public static final String SURVIVING_MUTANT_MARKER = PitUiActivator.PLUGIN_ID + ".survived"; + /** + * Id of the marker attribute fix hint + */ + public static final String SURVIVING_MUTANT_MARKER_ATTRIBUTE = PitUiActivator.PLUGIN_ID + ".fixHint"; + /** + * Id for markers of killed mutants + */ + public static final String KILLED_MUTANT_MARKER = PitUiActivator.PLUGIN_ID + ".killed"; + /** + * Id for markers of no coverage mutants + */ + public static final String NO_COVERAGE_MUTANT_MARKER = PitUiActivator.PLUGIN_ID + ".nocoverage"; + /** + * Id for markers of timeout mutants + */ + public static final String TIMEOUT_MUTANT_MARKER = PitUiActivator.PLUGIN_ID + ".timeout"; + /** + * Id for markers of non viable mutants + */ + public static final String NON_VIABLE_MUTANT_MARKER = PitUiActivator.PLUGIN_ID + ".nonViable"; + /** + * Id for markers of memory error mutants + */ + public static final String MEMORY_ERROR_MARKER = PitUiActivator.PLUGIN_ID + ".memError"; + /** + * Id for markers of run error mutants + */ + public static final String RUN_ERROR_MARKER = PitUiActivator.PLUGIN_ID + ".runError"; + + /** + * Uses the results to create a marker for each mutant + * @param results from pit which holds the information about the mutants + */ + @Override + public void handleResults(MutationsModel results) { + createMarkers(results); + } + + /** + * Extracts all mutants of the results to create a marker for each one + * @param results which hold the mutants + */ + private void createMarkers(MutationsModel results) { + removeOldMarkers(); + List mutations = ModelsVisitor.VISITOR.extractAllMutations(results); + int i = 0; + final IResource[] resources = new IResource[mutations.size()]; + final String[] types = new String[mutations.size()]; + @SuppressWarnings("unchecked") + final Map[] attributes = new Map[mutations.size()]; + + for (Mutation m : mutations) { + resources[i] = findClass(getProjectName(m), getClassName(m)); + types[i] = getType(m); + attributes[i] = new HashMap<>(); + attributes[i].put(IMarker.LINE_NUMBER, m.getLineNumber()); + attributes[i].put(IMarker.MESSAGE, m.getStatus().toString() + ": " + m.getDescription()); + + switch (types[i]) { + case SURVIVING_MUTANT_MARKER: + // TODO: if mutation survived, add hint how to probably kill it + attributes[i].put(SURVIVING_MUTANT_MARKER_ATTRIBUTE, "NOT IMPLEMENTED YET"); + attributes[i].put(IMarker.PRIORITY, IMarker.PRIORITY_HIGH); + break; + case TIMEOUT_MUTANT_MARKER: + case NO_COVERAGE_MUTANT_MARKER: + attributes[i].put(IMarker.PRIORITY, IMarker.PRIORITY_NORMAL); + break; + default: + // default. Used for killed mutants + attributes[i].put(IMarker.DONE, true); + attributes[i].put(IMarker.PRIORITY, IMarker.PRIORITY_LOW); + } + createMarker(resources[i], types[i], attributes[i]); + i++; + } + } + + /** + * Removes all old markers, which are Pitclipse markers + */ + private void removeOldMarkers() { + try { + IMarker[] marker = ResourcesPlugin.getWorkspace().getRoot().findMarkers(PITCLIPSE_MUTANT_MARKER, true, + IResource.DEPTH_INFINITE); + for (IMarker iMarker : marker) { + iMarker.delete(); + } + } catch (CoreException e) { + throw new RuntimeException("Error while deleting old markers occured.", e); + } + } + + /** + * Tries to create a marker for the given resource with the given type and + * attributes. + * @param iResource resource where to create the marker + * @param type of the marker to create + * @param attributes which should be added to the marker + * @return the created marker or null, if the marker could not be created + */ + private IMarker createMarker(IResource iResource, String type, Map attributes) { + try { + IMarker marker = iResource.createMarker(type); + marker.setAttributes(attributes); + return marker; + } catch (CoreException e) { + // could not create marker + return null; + } + } + + /** + * Maps the detection status of the given mutation to the corresponding marker + * id. If the detection status has no corresponding marker id the non viable + * marker is used. + * @param mutation which detection status is mapped + * @return the marker id which corresponds to the detection status of the mutant + */ + private String getType(Mutation mutation) { + switch (mutation.getStatus()) { + case SURVIVED: + return SURVIVING_MUTANT_MARKER; + case KILLED: + return KILLED_MUTANT_MARKER; + case NO_COVERAGE: + return NO_COVERAGE_MUTANT_MARKER; + case TIMED_OUT: + return TIMEOUT_MUTANT_MARKER; + case MEMORY_ERROR: + return MEMORY_ERROR_MARKER; + case RUN_ERROR: + return RUN_ERROR_MARKER; + default: + return NON_VIABLE_MUTANT_MARKER; + } + } + + /** + * Tries to find the class specified by its name in the project which name is + * given. + * @param projectName where the class file is located + * @param className which identifies the class + * @return the file handle or null, if the file was not found + */ + private IFile findClass(final String projectName, final String className) { + IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); + for (IProject project : root.getProjects()) { + if (project.getName().equals(projectName) && project.isOpen()) { + IJavaProject javaProject = JavaCore.create(project); + if (javaProject != null) { + try { + IType type = javaProject.findType(className); + return root.getFile(type.getPath()); + } catch (JavaModelException e) { + // Maybe type no longer exists. Do nothing + } + } + } + } + return null; + } + + /** + * Extracts the project name from the given mutation where it occured. + * @param mutation from which the project is desired + * @return the project name where the mutation occured + */ + private String getProjectName(Mutation mutation) { + return mutation.getClassMutations().getPackageMutations().getProjectMutations().getProjectName(); + } + + /** + * Extracts the class name from the given mutation where it occured. + * @param mutation from which the class is desired + * @return the class name where the mutation occured + */ + private String getClassName(Mutation mutation) { + return mutation.getClassMutations().getClassName(); + } + + /** + * Visitor which is used to extract all mutations from the mutation model + */ + private enum ModelsVisitor { + VISITOR; + public List extractAllMutations(MutationsModel mutationsModel) { + final LinkedList mutations = new LinkedList<>(); + for (Status s : mutationsModel.getStatuses()) { + VISITOR.visitStatus(s, mutations); + } + return mutations; + } + + public void visitStatus(Status status, List mutations) { + for (ProjectMutations p : status.getProjectMutations()) { + VISITOR.visitProject(p, mutations); + } + } + + public void visitProject(ProjectMutations projectMutations, List mutations) { + for (PackageMutations p : projectMutations.getPackageMutations()) { + VISITOR.visitPackage(p, mutations); + } + } + + public void visitPackage(PackageMutations packageMutations, List mutations) { + for (ClassMutations c : packageMutations.getClassMutations()) { + VISITOR.visitClass(c, mutations); + } + } + + public void visitClass(ClassMutations classMutations, List mutations) { + mutations.addAll(classMutations.getMutations()); + } + + } +} diff --git a/tests/org.pitest.pitclipse.ui.tests/src/org/pitest/pitclipse/ui/tests/PitclipsePitMutationsViewTest.java b/tests/org.pitest.pitclipse.ui.tests/src/org/pitest/pitclipse/ui/tests/PitclipsePitMutationsViewTest.java index 78f73d25..95c6cf89 100644 --- a/tests/org.pitest.pitclipse.ui.tests/src/org/pitest/pitclipse/ui/tests/PitclipsePitMutationsViewTest.java +++ b/tests/org.pitest.pitclipse.ui.tests/src/org/pitest/pitclipse/ui/tests/PitclipsePitMutationsViewTest.java @@ -1,12 +1,19 @@ package org.pitest.pitclipse.ui.tests; +import static org.junit.Assert.assertEquals; + +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.swtbot.swt.finder.junit.SWTBotJunit4ClassRunner; +import org.eclipse.swtbot.swt.finder.waits.DefaultCondition; import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; import org.pitest.pitclipse.ui.behaviours.steps.PitMutation; import org.pitest.pitclipse.ui.behaviours.steps.PitclipseSteps; +import org.pitest.pitclipse.ui.mutation.marker.PitclipseMutantMarkerFactory; /** * @author Lorenzo Bettini @@ -45,4 +52,49 @@ public void selectMutationOpensTheClassAtTheRightLineNumber() throws CoreExcepti pitclipseSteps.doubleClickMutationInMutationsView(mutation); pitclipseSteps.mutationIsOpened(BAR_CLASS + ".java", 7); } + + @Test + public void checkForExistingMutationMarkers() throws CoreException { + runPackageTest(FOO_BAR_PACKAGE, TEST_PROJECT); + coverageReportGenerated(2, 80, 0, 6, 0); + bot.waitUntil(new DefaultCondition() { + @Override + public boolean test() throws Exception { + return getPitclipseMarker().length > 0; + } + @Override + public String getFailureMessage() { + return "No pitclipse marker were found!"; + } + }); + IMarker[] marker = getPitclipseMarker(); + assertEquals(6, marker.length); + int numberOfKilled = 0, numberOfSurvived = 0, numberOfNoCoverage = 0, numberOfOthers = 0; + for (IMarker iMarker : marker) { + switch (iMarker.getType()) { + case PitclipseMutantMarkerFactory.KILLED_MUTANT_MARKER: + numberOfKilled++; + break; + case PitclipseMutantMarkerFactory.SURVIVING_MUTANT_MARKER: + numberOfSurvived++; + break; + case PitclipseMutantMarkerFactory.NO_COVERAGE_MUTANT_MARKER: + numberOfNoCoverage++; + break; + default: + numberOfOthers++; + break; + } + } + assertEquals(0, numberOfKilled); + assertEquals(2, numberOfSurvived); + assertEquals(4, numberOfNoCoverage); + assertEquals(0, numberOfOthers); + } + + private IMarker[] getPitclipseMarker() throws CoreException { + return ResourcesPlugin.getWorkspace().getRoot().findMarkers( + PitclipseMutantMarkerFactory.PITCLIPSE_MUTANT_MARKER, true, + IResource.DEPTH_INFINITE); + } }