Chapter 24. A Simple Editor to Exclude Tests

Auto-test runs all tests it finds inside a project. This is simple, but sometimes a user may want some control over the tests to be executed. In this chapter we will add support for excluding test cases from an auto-test run. We will implement a simple editor to define test cases that should be excluded. Editors are similar to views, but different enough to deserve a chapter of their own. We will see how to

  • Contribute an editor

  • Contribute an action to an editor

To support the exclusion feature, we need to permanently store a list of test cases associated with a project. Let's consider the different options Eclipse offers us to store state:

  • Preferences—Each plug-in has a preferences store accessed with Plugin.getPreferences(). Preferences map property names to primitive values. You change preference settings with preference pages contributed to the preferences dialog. Since preferences are not associated with a particular resource, they are global to the entire workspace. Preferences are therefore not an appropriate place to store a project's excluded tests.

  • Properties—Resources have properties keyed by a qualified name. There are two kinds of properties: session properties retrieved by IResource.getSessionProperty() and persistent properties retrieved by getPersistentProperty(). Persistent properties are intended to store resource-specific information that needs to persist across sessions.

    They could be used for exclusion lists. However, they have the limitation that they cannot be released to a repository. Their values cannot be shared with other members in the team. When we exclude a test case from an auto-test, the exclusion should be shared with other team members.

  • Plug-in metadata—The platform maintains a metadata area where a plug-in can store additional data. You retrieve the location of this area with Plugin.getStateLocation(). In the returned location you are free to create files and directories, but you are responsible for managing them. For example the Java core plug-in stores its source indices in this location. We could store information about excluded tests in this location. However, the metadata area isn't shareable across a team.

  • Files—Files are the simplest and most flexible way to store data. Files can easily be shared with other team members. We therefore use a simple file to store the exclusion list.

Excluded test cases will be defined in a file test.exclusion stored at the top-level of a project. The file lists the fully qualified names of the excluded tests, one per line. We'll create a test-filter editor to simplify the entry of the test names. It provides a command to show a dialog with all available tests inside the project. From this list the user selects a test case to be inserted into the exclusion list. Figure 24.1 shows the test-filter editor in action.

Files—

Let's start with a test case that specifies our desired behavior. When a failing test is excluded from auto-test, there should be no test failures. We add the test that captures this behavior to our builder tests:

Example . org.eclipse.contribution.junit.test/BuilderTest

org.eclipse.contribution.junit.test/BuilderTest
public void testAutoTestingFilter() throws Exception {
  IWorkspaceRunnable runnable= new IWorkspaceRunnable() {
    public void run(IProgressMonitor pm) throws CoreException {
      JUnitPlugin.getPlugin().addAutoBuildNature(
        testProject.getProject());
      IFile file= testProject.getProject().getFile(
        new Path("test.exclusions"));
      file.create(new ByteArrayInputStream(
        "pack1.FailTest".getBytes()), true, null);
      IPackageFragment pack= testProject.createPackage("pack1");
      testProject.createType(pack, "FailTest.java",
        "public class FailTest " +
        "extends junit.framework.TestCase {" +
        "    public void testFailure() {fail();}}");
    }
  };
  ResourcesPlugin.getWorkspace().run(runnable, null);
  IMarker[] markers= getFailureMarkers();
  assertEquals(0, markers.length);
}

We use the resources API to create the filter file and set its contents to the name of the failing test case. To make this test green, we augment the AutoTestBuilder with a call to a filter method that removes the types specified in the test.exclusion file:

Example . org.eclipse.contribution.junit/AutoTestBuilder

protected IProject[] build(int kind, Map args,
    IProgressMonitor monitor) throws CoreException {
  if (hasBuildErrors() || !enabled)
    return null;
  IJavaProject javaProject= JavaCore.create(getProject());
  IType[] types= new TestSearcher().findAll(javaProject, monitor);
  types= exclude(types);
  if (trace)
    printTestTypes(types);
  JUnitPlugin.getPlugin().run(types, javaProject);
  return null;
}

The implementation of the filter doesn't provide us with any additional interesting Eclipse insights. It is therefore included in Appendix C.

Where does the exclusion file test.exclusion come from? We could contribute a choice to the New Dialog using the org.eclipse.ui.newWizard extension point. However, to keep things simple the user would have to create the file using the standard new file wizard.

Contributing an Editor

Now let's make working with the exclusion file a bit friendlier by contributing a custom editor.

Contributing an Editor

Example . org.eclipse.jdt.ui/plugin.xml

org.eclipse.jdt.ui/plugin.xml
<extension point= "org.eclipse.ui.editors"
  id= "org.eclipse.jdt.ui.javaeditor">
  <editor
    id="org.eclipse.jdt.ui.CompilationUnitEditor"
    icon="icons/full/obj16/jcu_obj.gif"
    name="%JavaEditor.label"
    extensions="java"
    class="org.eclipse.jdt.internal.ui.javaeditor.
      CompilationUnitEditor"
    contributorClass="org.eclipse.jdt.internal.ui.javaeditor.
      CompilationUnitEditorActionContributor"
    default="true">
  </editor>
</extension>

The Java editor is contributed to files with the extension “java”.

An editor contribution requires two implementation classes. The class attribute defines the implementation of the editor part and the contributorClass specifies a separate class defining menu and toolbar contributions. This is a major difference between editors and views. A view has a single implementation class and this class handles toolbar contributions internally. Why are editor contributions factored out into a separate class? More than one editor of a particular kind can be open inside a workbench page. All open editors share the global menu and toolbar. Having a separate class for the object contributions allows the Eclipse workbench to share the editor contributions among editors of the same kind. Figure 24.2 illustrates this. It shows two open Java compilation unit editors and a text editor. The CompilationUnitEditorActionContributor is shared between the compilation unit editors.

Shared ActionContributor

Figure 24.2. Shared ActionContributor

The declaration of the Java editor contribution sets an attribute default to true. This is a hint that the contributed editor should be the default editor for *.java files. Consistent with the Diversity Rule, more than one editor can be contributed for a particular element. However, when opening a file, only a single editor should be opened. Eclipse handles the ambiguity by giving the user the option to open any of the registered editors in the Open With menu, but it makes it convenient to open the default editor directly, as shown in Figure 24.3.

Collector views—

This illustrates another rule:

  • USER ARBITRATION RULEWhen there are multiple applicable contributions, let the user decide which one to use.

So let's make our editor contribution for the test.excludes file. Since the file has a fixed name, we don't contribute the editor to the suffix only but to the entire file name. This is consistent with the Relevance Rule. We want to make our contribution as specific as possible. Instead of implementing a text editor from scratch we reuse the default text editor, but we contribute a custom contributorClass:

Example . org.eclipse.contribution.junit/plugin.xml

<extension point="org.eclipse.ui.editors">
  <editor
    id="org.eclipse.contribution.junit.TestExclusionEditor"
    icon="icons/testok.gif"
    name="Test Exclusions Editor"
    filenames="test.exclusion"
    class="org.eclipse.ui.editors.text.TextEditor"
    default="true"
    contributorClass="org.eclipse.contribution.junit.
      TestExclusionEditorActionContributor">
  </editor>
</extension>

Eclipse allows the user to modify the file-editor associations in the preferences. If we start up the run-time workbench after the above change, our editor shows up in the File Associations preference page, as shown in Figure 24.4.

org.eclipse.contribution.junit/plugin.xml

Even though we don't have to implement the editor ourselves, let's look at how its implementation differs from a view. An editor is opened on a particular element that becomes its input. An editor input is a lightweight description of the element. When a text editor is opened on a file, its input is an IFileEditorInput, as illustrated in Figure 24.5.

FileEditorInput Represents an IFile

Figure 24.5. FileEditorInput Represents an IFile

The input is passed to an editor part together with its site in the init() method.

In contrast to views, editors follow an open-modify-save cycle. The methods supporting saving are captured in the ISaveablePart interface. It defines methods like isDirty(), doSave() and doSaveAs(). When the dirty state of an editor changes we have to notify interested parties with firePropertyChange(PROP_DIRTY).

Next we'll implement the contributor class to add the actions for our editor.

Contributing a Contributor

Our contributor class extends the BasicTextEditorActionContributor and contributes an action, ExcludeTestAction, to append a test to be excluded.

Here is the test to verify that the action is contributed when our editor is opened. As for most of our other tests, the test is creating a TestProject in setUp() that is then disposed in tearDown().

Example . org.eclipse.contribution.junit.test/ExclusionEditorTest

org.eclipse.contribution.junit.test/ExclusionEditorTest
public class ExclusionEditorTest extends TestCase {
  private TestProject testProject;

  protected void setUp() throws Exception {
    testProject= new TestProject();
  }

  protected void tearDown() throws Exception {
    getPage().closeAllEditors(false);
    testProject.dispose();
  }
}

The actual test creates a test.exclusion file and opens it in the editor.

Example . org.eclipse.contribution.junit.test/ExclusionEditorTest

org.eclipse.contribution.junit.test/ExclusionEditorTest
public void testEditorContribution() throws CoreException {
  IFile file= testProject.getProject().getFile(
    new Path("test.exclusion"));
  file.create(new ByteArrayInputStream("".getBytes()), true, null);
  IEditorPart part= getPage().openEditor(file);
  assertTrue(findContributedAction(part));
}

To verify that the ExcludeTestAction is contributed to the toolbar, we have to reach into the toolbar manager and look for our contributed action:

Example . org.eclipse.contribution.junit.test/ExclusionEditorTest

org.eclipse.contribution.junit.test/ExclusionEditorTest
private boolean findContributedAction(IEditorPart part) {
  IActionBars actionBars= part.getEditorSite().getActionBars();
  IToolBarManager manager= actionBars.getToolBarManager();
  IContributionItem[] items= manager.getItems();
  for (int i= 0; i < items.length; i++) {
    IContributionItem item= items[i];
    if (item instanceof ActionContributionItem) {
      IAction a=((ActionContributionItem)item).getAction();
      if (a.getClass().equals(ExcludeTestAction.class))
        return true;
    }
  }
  return false;
}

Now let's implement the contributor class. It manages the action we want to contribute:

Example . org.eclipse.contribution.junit/TestExclusionEditorActionContributor

public class TestExclusionEditorActionContributor
    extends BasicTextEditorActionContributor {
  private ExcludeTestAction excludeAction;

  public TestExclusionEditorActionContributor() {
    excludeAction= new ExcludeTestAction();
  }
...

We contribute the excludeAction to the Edit menu and append it to the local toolbar by extending the corresponding contribution methods:

Example . org.eclipse.contribution.junit/TestExclusionEditorActionContributor

public void contributeToToolBar(IToolBarManager manager) {
  super.contributeToToolBar(manager);
  manager.add(new Separator());
  manager.add(excludeAction);
}

public void contributeToMenu(IMenuManager menu) {
  super.contributeToMenu(menu);
  IMenuManager editMenu= menu.findMenuUsingPath(
    IWorkbenchActionConstants.M_EDIT);
  if (editMenu != null) {
    editMenu.add(new Separator());
    editMenu.add(excludeAction);
  }
}

Notice that we contribute the same action object to both the menu and the toolbar. When changing an attribute of an action (for example, its enabled state), both the menu and toolbar are automatically updated.

Since the contributor is shared among editors of the same kind we have to implement setActiveEditor(). This method is called whenever the contributor's editor has changed. We check that the new editor is a TextEditor and inform our action that it should now target a different editor:

Example . org.eclipse.contribution.junit/TestExclusionEditorActionContributor

public void setActiveEditor(IEditorPart target) {
  super.setActiveEditor(target);
  ITextEditor editor= null;
  if (target instanceof ITextEditor)
    editor= (ITextEditor)target;
  excludeAction.setActiveEditor(editor);
}

Finally, to enable early garbage collection of editors when the contributor is disposed, we make sure that the action clears the reference to the active editor:

Example . org.eclipse.contribution.junit/TestExclusionEditorActionContributor

public void dispose() {
  super.dispose();
  excludeAction.setActiveEditor(null);
}

Next, let's implement the ExcludeTestAction. It is an ordinary JFace action and it keeps a reference to its target editor:

Example . org.eclipse.contribution.junit/ExcludeTestAction

public class ExcludeTestAction extends Action {
  private ITextEditor editor;
  public ExcludeTestAction() {
    setText("Exclude Test");
    setToolTipText("Exclude a Test Case");
    setImageDescriptor(createImage("icons/test.gif"));
  }

  public void setActiveEditor(ITextEditor target) {
    editor= target;
  }
  //...
}

We create the images in the helper method createImage() which retrieves the images relative to the installed plug-in location:

Example . org.eclipse.contribution.junit/ExcludeTestAction

private ImageDescriptor createImage(String path) {
  URL url= JUnitPlugin.getPlugin().getDescriptor().getInstallURL();
  ImageDescriptor descriptor= null;
  try {
    descriptor= ImageDescriptor.createFromURL(new URL(url, path));
  } catch (MalformedURLException e) {
    descriptor= ImageDescriptor.getMissingImageDescriptor();
  }
  return descriptor;
}

When the action is run, it allows the user to select a test. Once the user makes a choice, it appends the selected test name to the active editor:

Example . org.eclipse.contribution.junit/ExcludeTestAction

public void run() {
  IType type= selectTest();
  if (type != null)
    appendTest(type.getFullyQualifiedName());
}

The method selectTest() uses the TestSearcher to find all the tests inside the project and presents the list in a dialog. As we have seen above, the input to a text editor is a IFileEditorInput from which we can navigate to the containing Java project:

Example . org.eclipse.contribution.junit/ExcludeTestAction

private IType selectTest() {
  IEditorInput input= editor.getEditorInput();
  if (input instanceof IFileEditorInput) {
    IFile file= ((IFileEditorInput)input).getFile();
    IJavaProject jproject= JavaCore.create(file.getProject());
    try {
      IType[] types= new TestSearcher().findAll(jproject,
        new NullProgressMonitor());
      return showDialog(types);
    } catch (JavaModelException e) {
    }
  }
  return null;
}

To show the list of tests, we use an ElementListSelectionDialog with a JFace label provider to define the appearance of the elements in the dialog. The Java development tools provide a standard JavaElementLabelProvider that we use to render the types.

Example . org.eclipse.contribution.junit/ExcludeTestAction

private IType showDialog(IType[] types) {
  ElementListSelectionDialog dialog=
    new ElementListSelectionDialog(
      editor.getSite().getShell(),
      new JavaElementLabelProvider());
  dialog.setTitle("Exclude Test");
  dialog.setMessage("Select Test to exclude:");
  dialog.setElements(types);
  if (dialog.open() == Window.OK)
    return (IType)dialog.getFirstResult();
  return null;
}

Finally, we are ready to insert the fully qualified type name into the editor. Eclipse editors use IDocuments to store their contents. A text editor has a document provider. It knows which document is associated with a particular editor input. Once we have the document we can insert the text:

Example . org.eclipse.contribution.junit/ExcludeTestAction

private void appendTest(String testName) {
  IDocumentProvider provider= editor.getDocumentProvider();
  IEditorInput input= editor.getEditorInput();
  IDocument document= provider.getDocument(input);
  if (document == null)
    return;
  try {
    document.replace(document.getLength(), 0, testName+"
");
  } catch (BadLocationException e) {
  }
}

This concludes the implementation of our simple editor. Let's review this chapter:

  • We have contributed a text editor for the exclusion file.

  • We have shown how contributions in editors are done and how they differ from contribution to views.

In the next chapter we will revisit the ResultView.

Forward Pointers

  • Assigning shortcuts—To assign a user-configurable keyboard shortcut to an editor action, you use the org.eclipse.ui.commands extension point.

  • Action set part associations—Some actions should be applicable in both views and editors. For example, most Java actions manipulating the source code are available from both the Java views and the Java editors. You can contribute such actions in an action set and then define in which workbench parts the action set should be visible. See the org.eclipse.ui.actionSetPartAssociations extension point.

  • Content outline—For editors with more complex contents you want to populate the content outline. To do so, you have to implement getAdapter() and return a outline page when the getAdapter() is called with IContentOutlinePage.class.

  • Navigation history—Workbench editors keep a navigation history so that you can navigate back and forth between your open editors. If you implement a custom editor you also want to support the navigation history by implementing INavigationLocationProvider.

  • Multi-page editors—MultiPageEditorPart provides support for creating an editor with multiple pages, where each page contains an editor.

  • New wizards—Use the extension point org.eclipse.ui.newWizards to register wizards for creating new things like files, folders or projects with the New Dialog.

  • JFace text—Provides a framework, org.eclipse.jface.text.*, for manipulating text syntactically. It offers support for syntax coloring, content assistance, formatting, etc. For example, we could leverage JFace text to replace our dialog to select a test to exclude with a much more convenient code-assist style user interface as used in the Java editor to complete code (Figure 24.6).

    JFace text—

    To do so you implement a TextEditor subclass that is configured with a SourceViewerConfiguration that defines a custom content assistant. The content assistant uses an IContentAssistProcessor that knows how to complete test class names.

  • Org.eclipse.ui.texteditor.*—. Provides a framework for text editors.

  • Contributing to an editor's menu and toolbar actions—The extension point org.eclipse.ui.editorActions allows you to contribute to the menu and toolbar of an existing editor. For example, to contribute an action to our exclusion editor's toolbar you add the following declaration to the manifest:

    <extension point="org.eclipse.ui.editorActions">
      <editorContribution
        targetID="org.eclipse.contribution.junit.
           TestExclusionEditor"
        id="contributedId">
        <action
           label= "..."
           class= "..."
            toolbarPath= "..." >
        </action>
      </editorContribution>
    </extension>
    
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset