Commit f289f6ad authored by Philip Makedonski's avatar Philip Makedonski
Browse files

+ added option to run generated JUnit tests directly from TDL file editor, #163

parent 9198cda4
Loading
Loading
Loading
Loading
Loading
+6 −1
Original line number Diff line number Diff line
@@ -15,5 +15,10 @@ Require-Bundle: org.eclipse.core.runtime,
 org.eclipse.emf.ecore,
 org.etsi.mts.tdl.model,
 org.etsi.mts.tdl.execution.java.codegen,
 junit-jupiter-api
 junit-jupiter-api,
 org.eclipse.debug.core,
 org.eclipse.debug.ui,
 org.eclipse.jdt.core,
 org.eclipse.jdt.launching,
 org.eclipse.xtext
+90 −0
Original line number Diff line number Diff line
@@ -62,4 +62,94 @@
      </page>
   </extension>

	
   <!-- TODO: update ids 
   		═══════════════════════════════════════════════════════════════
        1.  Declare the command
        ═══════════════════════════════════════════════════════════════ -->
   <extension point="org.eclipse.ui.commands">
      <command
            defaultHandler="org.etsi.mts.tdl.execution.java.eclipse.commands.RunJUnitTestHandler"
            id="com.example.tdltx.junitrunner.runJUnitTest"
            name="Run Specific JUnit Test…"
            description="Select and run a predefined JUnit test for this .tdltx file">
      </command>
   </extension>

   <!-- ═══════════════════════════════════════════════════════════════
        2.  Bind the command to its handler
        ═══════════════════════════════════════════════════════════════ -->
   <extension point="org.eclipse.ui.handlers">
      <handler
            commandId="com.example.tdltx.junitrunner.runJUnitTest"
            class="org.etsi.mts.tdl.execution.java.eclipse.commands.RunJUnitTestHandler">
         <!-- Only active when the active editor has a .tdltx file open -->
         <activeWhen>
            <with variable="activeEditorInput">
               <adapt type="org.eclipse.core.resources.IFile">
                  <test
                     property="org.eclipse.core.resources.extension"
                     value="tdltx"/>
               </adapt>
            </with>
         </activeWhen>
      </handler>
   </extension>

   <!-- ═══════════════════════════════════════════════════════════════
        3.  Add the menu entry to the editor context menu
            The editorContribution targets *all* editors; the visibility
            expression restricts it to .tdltx files.
        ═══════════════════════════════════════════════════════════════ -->
   <extension point="org.eclipse.ui.menus">

      <!-- Context menu that appears when right-clicking inside any editor -->
      <menuContribution
			locationURI="popup:#TextEditorContext?after=group.edit">

               <!-- TODO: add icon -->
         <command
               commandId="com.example.tdltx.junitrunner.runJUnitTest"
               label="Run Specific Test…"
               icon="icons/junit_run.png"
               style="push">
            <visibleWhen checkEnabled="false">
               <with variable="activeEditorInput">
                  <adapt type="org.eclipse.core.resources.IFile">
                     <test
                        property="org.eclipse.core.resources.extension"
                        value="tdltx"/>
                  </adapt>
               </with>
            </visibleWhen>
         </command>

      </menuContribution>

      <!-- Also add to the Project Explorer / Package Explorer context menu
           when a .tdltx file is selected -->
      <menuContribution
            locationURI="popup:org.eclipse.ui.navigator.ProjectExplorer#PopupMenu?after=additions">

         <separator name="tdltxSeparatorNav" visible="true"/>

         <command
               commandId="com.example.tdltx.junitrunner.runJUnitTest"
               label="Run Specific JUnit Test…"
               icon="icons/junit_run.png"
               style="push">
            <visibleWhen checkEnabled="false">
               <iterate ifEmpty="false">
                  <adapt type="org.eclipse.core.resources.IFile">
                     <test
                        property="org.eclipse.core.resources.extension"
                        value="tdltx"/>
                  </adapt>
               </iterate>
            </visibleWhen>
         </command>

      </menuContribution>

   </extension>
</plugin>
+30 −0
Original line number Diff line number Diff line
package org.etsi.mts.tdl.execution.java.eclipse.commands;

import org.eclipse.ui.plugin.AbstractUIPlugin;
import org.osgi.framework.BundleContext;

/**
 * Standard OSGi/Eclipse bundle activator.
 */
public class Activator extends AbstractUIPlugin {

    public static final String PLUGIN_ID = "com.example.tdltx.junitrunner"; //$NON-NLS-1$

    private static Activator plugin;

    @Override
    public void start(BundleContext context) throws Exception {
        super.start(context);
        plugin = this;
    }

    @Override
    public void stop(BundleContext context) throws Exception {
        plugin = null;
        super.stop(context);
    }

    public static Activator getDefault() {
        return plugin;
    }
}
 No newline at end of file
+66 −0
Original line number Diff line number Diff line
package org.etsi.mts.tdl.execution.java.eclipse.commands;

/**
 * Describes one predefined JUnit test that can be selected and launched.
 *
 * <p>Extend {@link PredefinedTests} to add more entries; no other class
 * needs to change.
 */
public final class JUnitTestDescriptor {

    /** Human-readable label shown in the selection dialog. */
    private final String label;

    /**
     * Fully-qualified class name of the JUnit test class,
     * e.g. {@code com.example.myapp.tests.MyFeatureTest}.
     */
    private final String testClassName;

    /**
     * Optional method name. When {@code null} the whole class is run.
     * When set, only that single test method is run,
     * e.g. {@code testParseValidDocument}.
     */
    private final String testMethodName;

    private final String testObjective;

    // -----------------------------------------------------------------------

    /**
     * Creates a descriptor for running an entire test class.
     */
    public JUnitTestDescriptor(String label, String testClassName) {
        this(label, testClassName, null, null);
    }

    public JUnitTestDescriptor(String label, String testClassName, String testMethodName) {
        this(label, testClassName, testMethodName, null);
    }

    
    /**
     * Creates a descriptor for running a single test method.
     */
    public JUnitTestDescriptor(String label, String testClassName, String testMethodName, String testObjective) {
        this.label          = label;
        this.testClassName  = testClassName;
        this.testMethodName = testMethodName;
        this.testObjective = testObjective;
    }

    // -----------------------------------------------------------------------

    public String getLabel()          { return label; }
    public String getTestClassName()  { return testClassName; }
    public String getTestMethodName() { return testMethodName; }
    public String getTestObjective()  { return testObjective; }

    /** Returns {@code true} when only a single method should be executed. */
    public boolean hasSingleMethod()  { return testMethodName != null && !testMethodName.isEmpty(); }
    public boolean hastestObjective() { return testObjective != null && !testObjective.isEmpty(); }

    @Override
    public String toString() { return label; }
}
 No newline at end of file
+272 −0
Original line number Diff line number Diff line
package org.etsi.mts.tdl.execution.java.eclipse.commands;

import java.util.List;

import org.eclipse.core.commands.AbstractHandler;
import org.eclipse.core.commands.ExecutionEvent;
import org.eclipse.core.commands.ExecutionException;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Status;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.ILaunchConfiguration;
import org.eclipse.debug.core.ILaunchConfigurationType;
import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
import org.eclipse.debug.core.ILaunchManager;
import org.eclipse.debug.ui.DebugUITools;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants;
import org.eclipse.jface.dialogs.ErrorDialog;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IFileEditorInput;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.handlers.HandlerUtil;
import org.eclipse.ui.part.FileEditorInput;
import org.eclipse.xtext.EcoreUtil2;
import org.eclipse.xtext.resource.XtextResourceSet;
import org.etsi.mts.tdl.Package;
import org.etsi.mts.tdl.TestDescription;

import com.google.inject.Guice;
import com.google.inject.Injector;

/**
 * Handles the "Run Specific JUnit Test…" command.
 *
 * <ol>
 *   <li>Determines the {@link IProject} from either the active editor or the
 *       current selection in the Navigator/Package Explorer.</li>
 *   <li>Opens {@link SelectJUnitTestDialog} with the entries from
 *       {@link PredefinedTests}.</li>
 *   <li>Creates (or re-uses) a JUnit launch configuration for the chosen
 *       test and launches it in run mode.</li>
 * </ol>
 */
public class RunJUnitTestHandler extends AbstractHandler {

    /** ID of the JUnit 5 launch config type bundled with org.eclipse.jdt.junit. */
    private static final String JUNIT_LAUNCH_CONFIG_TYPE =
            "org.eclipse.jdt.junit.launchconfig"; //$NON-NLS-1$

    // -----------------------------------------------------------------------

    @Override
    public Object execute(ExecutionEvent event) throws ExecutionException {
        IWorkbenchWindow window = HandlerUtil.getActiveWorkbenchWindowChecked(event);

        // ── 1. Resolve the project ──────────────────────────────────────────
        IProject project = resolveProject(event);
        if (project == null) {
            showError(window, "No Project Found",
                    "Could not determine the project for the selected .tdltx file.");
            return null;
        }
        
        //TODO: this should be extracted as it is duplicated across all handlers..

		ISelection selection = HandlerUtil.getCurrentSelection(event);
		IEditorInput input = HandlerUtil.getActiveEditorInput(event);
		IFile file = null;
		if (input != null && input instanceof FileEditorInput) {
			file = ((FileEditorInput) input).getFile();
		} else if (selection !=null && selection instanceof IStructuredSelection) {
			IStructuredSelection structuredSelection = (IStructuredSelection) selection;
			Object firstElement = structuredSelection.getFirstElement();
			if (firstElement instanceof IFile) {
				file = (IFile) firstElement;
			}
		}
		
		if (file == null) {
			//TODO: error message
			return null;
		}
		URI uri = URI.createPlatformResourceURI(file.getFullPath().toString(), true);
		Injector injector = Guice.createInjector();
		XtextResourceSet resourceSet = injector.getInstance(XtextResourceSet.class);
		XtextResourceSet rs = resourceSet;
		Resource r = rs.getResource(uri, true);
		EcoreUtil.resolveAll(r);

		Package p = (Package) r.getContents().get(0);
		List<TestDescription> tests = EcoreUtil2.getAllContentsOfType(r, TestDescription.class);
        // ── 2. Open the selection dialog ────────────────────────────────────
		
        List<JUnitTestDescriptor> all = tests.stream()
        		.filter(TestDescription::isIsLocallyOrdered)
        		.map(t -> new JUnitTestDescriptor(
        				t.getName(), //DONE: add test objective?
        				//TODO: why is this specific to mec.testcases?
        				"org.etsi.mts.tdl.tests.mec.testcases."
        				+p.getName().toLowerCase()
        				+"."
        				+t.getName(), 
        				"test_"+t.getName(),
        				t.getTestObjective().isEmpty() ? null : t.getTestObjective().getFirst().getDescription())
        			)
        		.toList();
        
        //TODO: refine display
		SelectJUnitTestDialog dialog =
                new SelectJUnitTestDialog(window.getShell(), all);

        if (dialog.open() != IDialogConstants.OK_ID) {
            return null; // user cancelled
        }

        JUnitTestDescriptor chosen = dialog.getSelectedTest();
        if (chosen == null) {
            return null;
        }

        // ── 3. Launch the chosen test ────────────────────────────────────────
        try {
            launchJUnitTest(project, chosen);
        } catch (CoreException e) {
            Activator.getDefault().getLog().log(
                new Status(IStatus.ERROR, Activator.PLUGIN_ID,
                           "Failed to launch JUnit test: " + chosen.getLabel(), e));
            ErrorDialog.openError(window.getShell(), "Launch Failed",
                    "An error occurred while launching the JUnit test.", e.getStatus());
        }

        return null;
    }

    // -----------------------------------------------------------------------
    //  Project resolution helpers
    // -----------------------------------------------------------------------

    /**
     * Tries to find an {@link IProject} from the active editor input first,
     * then falls back to the current workbench selection.
     */
    private IProject resolveProject(ExecutionEvent event) {

        // Try editor input
        IEditorInput editorInput = HandlerUtil.getActiveEditorInput(event);
        if (editorInput instanceof IFileEditorInput) {
            IFile file = ((IFileEditorInput) editorInput).getFile();
            if (file != null) {
                return file.getProject();
            }
        }

        // Try current selection (Navigator / Package Explorer)
        ISelection selection = HandlerUtil.getCurrentSelection(event);
        if (selection instanceof IStructuredSelection) {
            Object first = ((IStructuredSelection) selection).getFirstElement();
            if (first instanceof IFile) {
                return ((IFile) first).getProject();
            }
            if (first instanceof IProject) {
                return (IProject) first;
            }
        }

        return null;
    }

    // -----------------------------------------------------------------------
    //  JUnit launch configuration
    // -----------------------------------------------------------------------

    /**
     * Creates (or reuses an existing) JUnit launch configuration and runs it.
     *
     * <p>Configuration names follow the pattern:
     * {@code <ProjectName> – <TestLabel>}
     * so multiple predefined tests can coexist without colliding.</p>
     */
    private void launchJUnitTest(IProject project, JUnitTestDescriptor test)
            throws CoreException {

        ILaunchManager   manager  = DebugPlugin.getDefault().getLaunchManager();
        ILaunchConfigurationType type =
                manager.getLaunchConfigurationType(JUNIT_LAUNCH_CONFIG_TYPE);

        String configName = project.getName() + " – " + test.getLabel();

        // Re-use an existing config with the same name (avoids proliferation)
        ILaunchConfiguration existing = findExistingConfig(manager, type, configName);
        ILaunchConfiguration config;

        if (existing != null) {
            config = existing;
        } else {
            config = createLaunchConfig(type, configName, project, test);
        }

        DebugUITools.launch(config, ILaunchManager.RUN_MODE);
    }

    private ILaunchConfiguration findExistingConfig(
            ILaunchManager manager,
            ILaunchConfigurationType type,
            String name) throws CoreException {

        for (ILaunchConfiguration cfg : manager.getLaunchConfigurations(type)) {
            if (cfg.getName().equals(name)) {
                return cfg;
            }
        }
        return null;
    }

    private ILaunchConfiguration createLaunchConfig(
            ILaunchConfigurationType type,
            String name,
            IProject project,
            JUnitTestDescriptor test) throws CoreException {

        ILaunchConfigurationWorkingCopy wc = type.newInstance(null, name);

        // Project
        wc.setAttribute(
            IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME,
            project.getName());

        // Test class
        wc.setAttribute(
            IJavaLaunchConfigurationConstants.ATTR_MAIN_TYPE_NAME,
            test.getTestClassName());

        // Optional: single test method
        if (test.hasSingleMethod()) {
            wc.setAttribute(
                "org.eclipse.jdt.junit.TESTNAME",  //$NON-NLS-1$
                test.getTestMethodName());
        }

        // JUnit 5 runner (change to "4" for JUnit 4, or "" for auto-detect)
        wc.setAttribute("org.eclipse.jdt.junit.TEST_KIND", "org.eclipse.jdt.junit.loader.junit5"); //$NON-NLS-1$ //$NON-NLS-2$

        // Keep source locator
        wc.setAttribute(
            IJavaLaunchConfigurationConstants.ATTR_SOURCE_PATH_PROVIDER,
            "org.eclipse.jdt.launching.sourceLocator.JavaSourceLookupDirector"); //$NON-NLS-1$

        return wc.doSave();
    }

    // -----------------------------------------------------------------------
    //  Utility
    // -----------------------------------------------------------------------

    private void showError(IWorkbenchWindow window, String title, String message) {
        ErrorDialog.openError(
            window.getShell(), title, message,
            new Status(IStatus.ERROR, Activator.PLUGIN_ID, message));
    }
}
 No newline at end of file
Loading