/*
* Copyright 2015 Nokia Solutions and Networks
* Licensed under the Apache License, Version 2.0,
* see license.txt file for details.
*/
package org.robotframework.ide.eclipse.main.plugin.views.execution;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.inject.Inject;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import org.eclipse.e4.core.services.events.IEventBroker;
import org.eclipse.e4.ui.di.Focus;
import org.eclipse.jface.action.IToolBarManager;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.layout.GridLayoutFactory;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.text.TextSelection;
import org.eclipse.jface.viewers.AbstractTreeViewer;
import org.eclipse.jface.viewers.IDoubleClickListener;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.TreeSelection;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.ViewerColumnsFactory;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.ui.IEditorDescriptor;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IEditorRegistry;
import org.eclipse.ui.IViewPart;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.part.FileEditorInput;
import org.rf.ide.core.execution.ExecutionElement;
import org.rf.ide.core.execution.ExecutionElement.ExecutionElementType;
import org.rf.ide.core.execution.Status;
import org.robotframework.ide.eclipse.main.plugin.RedImages;
import org.robotframework.ide.eclipse.main.plugin.RedPlugin;
import org.robotframework.ide.eclipse.main.plugin.launch.RobotTestExecutionService;
import org.robotframework.ide.eclipse.main.plugin.launch.RobotTestExecutionService.RobotTestExecutionListener;
import org.robotframework.ide.eclipse.main.plugin.launch.RobotTestExecutionService.RobotTestsLaunch;
import org.robotframework.ide.eclipse.main.plugin.model.RobotCase;
import org.robotframework.ide.eclipse.main.plugin.model.RobotFileInternalElement.DefinitionPosition;
import org.robotframework.ide.eclipse.main.plugin.model.RobotSuiteFile;
import org.robotframework.ide.eclipse.main.plugin.model.locators.ContinueDecision;
import org.robotframework.ide.eclipse.main.plugin.model.locators.TestCasesDefinitionLocator;
import org.robotframework.ide.eclipse.main.plugin.model.locators.TestCasesDefinitionLocator.TestCaseDetector;
import org.robotframework.ide.eclipse.main.plugin.tableeditor.RobotFormEditor;
import org.robotframework.ide.eclipse.main.plugin.tableeditor.RobotFormEditor.RobotEditorOpeningException;
import org.robotframework.ide.eclipse.main.plugin.tableeditor.source.SuiteSourceEditor;
import org.robotframework.ide.eclipse.main.plugin.views.execution.ExecutionElementsStore.ExecutionElementsStoreListener;
import org.robotframework.red.actions.CollapseAllAction;
import org.robotframework.red.actions.ExpandAllAction;
import org.robotframework.red.graphics.ImagesManager;
import org.robotframework.red.swt.SwtThread;
import org.robotframework.red.viewers.Selections;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
/**
* @author mmarzec
*
*/
@SuppressWarnings({ "PMD.GodClass", "PMD.TooManyMethods" })
public class ExecutionView {
public static final String ID = "org.robotframework.ide.ExecutionView";
@Inject
protected IEventBroker eventBroker;
private final RobotTestExecutionService executionService;
private Label passCounterLabel;
private Label failCounterLabel;
private int passCounter = 0;
private int failCounter = 0;
private TreeViewer executionViewer;
private ExecutionViewContentProvider executionViewContentProvider;
private StyledText messageText;
private ShowFailedOnlyAction showFailedAction;
private RerunFailedOnlyAction rerunFailedOnlyAction;
private final List<ExecutionStatus> executionViewerInput = new ArrayList<>();
private final LinkedList<ExecutionStatus> suitesStack = new LinkedList<>();
private RobotTestExecutionListener executionListener;
private final ExecutionElementsStoreListener storeListener =
(store, elem) -> SwtThread.syncExec(() -> executionEvent(elem));
public ExecutionView() {
this(RedPlugin.getTestExecutionService());
}
@VisibleForTesting
ExecutionView(final RobotTestExecutionService executionService) {
this.executionService = executionService;
}
@PostConstruct
public void postConstruct(final Composite parent, final IViewPart part) {
GridDataFactory.fillDefaults().grab(true, true).applyTo(parent);
GridLayoutFactory.fillDefaults().numColumns(1).applyTo(parent);
final Composite labelsComposite = new Composite(parent, SWT.NONE);
GridLayoutFactory.fillDefaults().numColumns(4).applyTo(labelsComposite);
GridDataFactory.fillDefaults().grab(true, false).indent(2, 2).applyTo(labelsComposite);
final Label passImageLabel = new Label(labelsComposite, SWT.NONE);
passImageLabel.setImage(ImagesManager.getImage(RedImages.getSuccessImage()));
passCounterLabel = new Label(labelsComposite, SWT.NONE);
GridDataFactory.fillDefaults().hint(70, 15).applyTo(passCounterLabel);
passCounterLabel.setText("Passed: 0");
final Label failImageLabel = new Label(labelsComposite, SWT.NONE);
failImageLabel.setImage(ImagesManager.getImage(RedImages.getErrorImage()));
failCounterLabel = new Label(labelsComposite, SWT.NONE);
GridDataFactory.fillDefaults().hint(70, 15).applyTo(failCounterLabel);
failCounterLabel.setText("Failed: 0");
executionViewer = new TreeViewer(parent);
executionViewer.getTree().setHeaderVisible(false);
GridDataFactory.fillDefaults().grab(true, true).applyTo(executionViewer.getTree());
GridLayoutFactory.fillDefaults().numColumns(1).applyTo(executionViewer.getTree());
executionViewContentProvider = new ExecutionViewContentProvider();
executionViewer.setContentProvider(executionViewContentProvider);
executionViewer.addSelectionChangedListener(createSelectionChangedListener());
executionViewer.addDoubleClickListener(createDoubleClickListener());
final Menu menu = createContextMenu();
executionViewer.getTree().addMouseListener(createMouseListener(menu));
ViewerColumnsFactory.newColumn("")
.withWidth(300)
.shouldGrabAllTheSpaceLeft(true)
.labelsProvidedBy(new ExecutionViewLabelProvider())
.createFor(executionViewer);
setViewerInput();
messageText = new StyledText(parent, SWT.H_SCROLL | SWT.V_SCROLL);
messageText.setFont(JFaceResources.getTextFont());
GridDataFactory.fillDefaults().grab(true, false).indent(3, 0).hint(0, 50).applyTo(messageText);
GridLayoutFactory.fillDefaults().applyTo(messageText);
messageText.setEditable(false);
messageText.setAlwaysShowScrollBars(false);
createToolbarActions(part.getViewSite().getActionBars().getToolBarManager());
setInput();
}
private void setInput() {
// synchronize on service, so that any thread which would like to start another launch
// will have to wait
synchronized (executionService) {
executionListener = new ExecutionListener(storeListener);
executionService.addExecutionListener(executionListener);
final Optional<RobotTestsLaunch> lastLaunch = executionService.getLastLaunch();
if (lastLaunch.isPresent()) {
final RobotTestsLaunch launch = lastLaunch.get();
// this launch may be currently running, so we have to synchronize in order
// to get proper state of messages, as other threads may change it in the meantime
synchronized (launch) {
final ExecutionElementsStore elementsStore = launch.getExecutionData(ExecutionElementsStore.class,
ExecutionElementsStore::new);
elementsStore.addStoreListener(storeListener);
final List<ExecutionElement> currentElements = elementsStore.getElements();
SwtThread.syncExec(() -> executionEvents(currentElements));
}
}
}
}
@Focus
public void onFocus() {
executionViewer.getControl().setFocus();
}
@PreDestroy
public void dispose() {
synchronized (executionService) {
executionService.removeExecutionListener(executionListener);
executionService.forEachLaunch(launch -> launch.getExecutionData(ExecutionElementsStore.class)
.ifPresent(store -> store.removeStoreListener(storeListener)));
}
}
private void executionEvents(final List<ExecutionElement> executionElements) {
for (final ExecutionElement executionElement : executionElements) {
executionEvent(executionElement);
}
}
private void executionEvent(final ExecutionElement executionElement) {
if (isSuiteStartEvent(executionElement)) {
handleSuiteStartEvent(executionElement);
} else if (isTestStartEvent(executionElement)) {
handleTestStartEvent(executionElement);
} else if (isSuiteEndEvent(executionElement)) {
handleSuiteEndEvent(executionElement);
} else if (isTestEndEvent(executionElement)) {
handleTestEndEvent(executionElement);
} else if (isOutputFileEvent(executionElement)) {
handleOutputFileEvent(executionElement);
}
refreshView();
}
private void clearView() {
suitesStack.clear();
executionViewerInput.clear();
setViewerInput();
passCounter = 0;
failCounter = 0;
messageText.setText("");
executionViewContentProvider.setFailedFilterEnabled(false);
showFailedAction.setChecked(false);
rerunFailedOnlyAction.setOutputFilePath(null);
refreshView();
}
private void refreshView() {
passCounterLabel.setText("Passed: " + passCounter);
failCounterLabel.setText("Failed: " + failCounter);
executionViewer.refresh();
}
private ISelectionChangedListener createSelectionChangedListener() {
return new ISelectionChangedListener() {
@Override
public void selectionChanged(final SelectionChangedEvent event) {
final String message = Selections
.getOptionalFirstElement((IStructuredSelection) event.getSelection(), ExecutionStatus.class)
.map(status -> Strings.nullToEmpty(status.getMessage()))
.orElse("");
messageText.setText(message);
}
};
}
private IDoubleClickListener createDoubleClickListener() {
return event -> Selections
.getOptionalFirstElement((IStructuredSelection) event.getSelection(), ExecutionStatus.class)
.ifPresent(status -> openExecutionStatusSourceFile(status));
}
private Menu createContextMenu() {
final Menu menu = new Menu(executionViewer.getTree());
final MenuItem gotoItem = new MenuItem(menu, SWT.PUSH);
gotoItem.setText("Go to File");
gotoItem.setImage(ImagesManager.getImage(RedImages.getGoToImage()));
gotoItem.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(final SelectionEvent event) {
final IStructuredSelection selection = (IStructuredSelection) executionViewer.getSelection();
Selections.getOptionalFirstElement(selection, ExecutionStatus.class)
.ifPresent(status -> openExecutionStatusSourceFile(status));
}
});
return menu;
}
private MouseAdapter createMouseListener(final Menu menu) {
return new MouseAdapter() {
@Override
public void mouseDown(final MouseEvent e) {
if (e.button == 3 && executionViewer.getTree().getSelectionCount() == 1) {
final TreeSelection selection = (TreeSelection) executionViewer.getSelection();
if (selection != null
&& ((ExecutionStatus) selection.getFirstElement()).getType() == ExecutionElementType.TEST) {
menu.setVisible(true);
}
}
}
};
}
private void openExecutionStatusSourceFile(final ExecutionStatus executionStatus) {
if (executionStatus == null || executionStatus.getSource() == null) {
return;
}
final IPath sourcePath = new Path(executionStatus.getSource());
final IFile sourceFile = ResourcesPlugin.getWorkspace().getRoot().getFileForLocation(sourcePath);
if (sourceFile == null || !sourceFile.exists()) {
return;
}
new TestCasesDefinitionLocator(sourceFile)
.locateTestCaseDefinition(createDetector(sourceFile, executionStatus.getName()));
}
private TestCaseDetector createDetector(final IFile sourceFile, final String caseName) {
return new TestCaseDetector() {
@Override
public ContinueDecision testCaseDetected(final RobotSuiteFile file, final RobotCase testCase) {
if (testCase.getName().equals(caseName)) {
final IEditorRegistry editorRegistry = PlatformUI.getWorkbench().getEditorRegistry();
final IEditorDescriptor desc = editorRegistry.findEditor(RobotFormEditor.ID);
try {
final IEditorPart editor = PlatformUI.getWorkbench()
.getActiveWorkbenchWindow()
.getActivePage()
.openEditor(new FileEditorInput(sourceFile), desc.getId());
if (editor instanceof RobotFormEditor) {
final SuiteSourceEditor sourcePage = ((RobotFormEditor) editor).activateSourcePage();
final DefinitionPosition position = testCase.getDefinitionPosition();
sourcePage.getSelectionProvider()
.setSelection(new TextSelection(position.getOffset(), position.getLength()));
}
} catch (final PartInitException e) {
throw new RobotEditorOpeningException("Unable to open editor for file: " + sourceFile.getName(),
e);
}
return ContinueDecision.STOP;
} else {
return ContinueDecision.CONTINUE;
}
}
};
}
private void createToolbarActions(final IToolBarManager toolBarManager) {
toolBarManager.add(new ExpandAllAction(executionViewer));
toolBarManager.add(new CollapseAllAction(executionViewer));
showFailedAction = new ShowFailedOnlyAction(executionViewer, executionViewContentProvider);
toolBarManager.add(showFailedAction);
toolBarManager.add(new RerunAction());
rerunFailedOnlyAction = new RerunFailedOnlyAction();
toolBarManager.add(rerunFailedOnlyAction);
}
private boolean isSuiteStartEvent(final ExecutionElement executionElement) {
return executionElement.getStatus() == null && executionElement.getType() == ExecutionElementType.SUITE;
}
private boolean isTestStartEvent(final ExecutionElement executionElement) {
return executionElement.getStatus() == null && executionElement.getType() == ExecutionElementType.TEST;
}
private boolean isSuiteEndEvent(final ExecutionElement executionElement) {
return executionElement.getStatus() != null && executionElement.getType() == ExecutionElementType.SUITE;
}
private boolean isTestEndEvent(final ExecutionElement executionElement) {
return executionElement.getStatus() != null && executionElement.getType() == ExecutionElementType.TEST;
}
private boolean isOutputFileEvent(final ExecutionElement executionElement) {
return executionElement.getType() == ExecutionElementType.OUTPUT_FILE;
}
private void handleSuiteStartEvent(final ExecutionElement executionElement) {
final ExecutionStatus newSuiteExecutionStatus = new ExecutionStatus(executionElement.getName(), Status.RUNNING,
executionElement.getType(), new ArrayList<ExecutionStatus>());
if (suitesStack.isEmpty()) {
suitesStack.add(newSuiteExecutionStatus);
executionViewerInput.add(newSuiteExecutionStatus);
setViewerInput();
} else {
final ExecutionStatus lastSuite = suitesStack.getLast();
newSuiteExecutionStatus.setParent(lastSuite);
newSuiteExecutionStatus.setSource(executionElement.getSource().getAbsolutePath());
lastSuite.addChildren(newSuiteExecutionStatus);
suitesStack.addLast(newSuiteExecutionStatus);
}
executionViewer.expandToLevel(newSuiteExecutionStatus, 1);
}
private void handleTestStartEvent(final ExecutionElement executionElement) {
final ExecutionStatus newTestExecutionStatus = new ExecutionStatus(executionElement.getName(), Status.RUNNING,
executionElement.getType(), new ArrayList<ExecutionStatus>());
if (!suitesStack.isEmpty()) {
final ExecutionStatus lastSuite = suitesStack.getLast();
newTestExecutionStatus.setParent(lastSuite);
newTestExecutionStatus.setSource(lastSuite.getSource());
lastSuite.addChildren(newTestExecutionStatus);
executionViewer.reveal(newTestExecutionStatus);
}
}
private void handleSuiteEndEvent(final ExecutionElement executionElement) {
if (!suitesStack.isEmpty()) {
final ExecutionStatus lastSuite = suitesStack.getLast();
final int elapsedTime = executionElement.getElapsedTime();
lastSuite.setElapsedTime(String.valueOf(((double) elapsedTime) / 1000));
final Status status = executionElement.getStatus();
lastSuite.setStatus(status);
if (suitesStack.size() > 1) {
suitesStack.removeLast();
if (status == Status.PASS) {
executionViewer.collapseToLevel(lastSuite, AbstractTreeViewer.ALL_LEVELS);
}
}
}
}
private void handleTestEndEvent(final ExecutionElement executionElement) {
if (!suitesStack.isEmpty()) {
final ExecutionStatus lastSuite = suitesStack.getLast();
final List<ExecutionStatus> lastSuiteChildren = lastSuite.getChildren();
final Status status = executionElement.getStatus();
final int elapsedTime = executionElement.getElapsedTime();
for (final ExecutionStatus executionStatus : lastSuiteChildren) {
if (executionStatus.getName().equals(executionElement.getName()) && executionStatus.getStatus() == Status.RUNNING) {
executionStatus.setStatus(status);
final String message = executionElement.getMessage();
if (message != null && !message.equals("")) {
executionStatus.setMessage(message);
}
executionStatus.setElapsedTime(String.valueOf(((double) elapsedTime) / 1000));
executionViewer.reveal(executionStatus);
break;
}
}
if (status == Status.PASS) {
passCounter++;
} else {
failCounter++;
}
}
}
private void handleOutputFileEvent(final ExecutionElement executionElement) {
rerunFailedOnlyAction.setOutputFilePath(executionElement.getName());
}
private void setViewerInput() {
executionViewer.setInput(executionViewerInput.toArray(new ExecutionStatus[executionViewerInput.size()]));
}
private class ExecutionListener implements RobotTestExecutionListener {
private final ExecutionElementsStoreListener storeListener;
ExecutionListener(final ExecutionElementsStoreListener storeListener) {
this.storeListener = storeListener;
}
@Override
public void executionStarting(final RobotTestsLaunch launch) {
SwtThread.syncExec(() -> clearView());
launch.getExecutionData(ExecutionElementsStore.class, ExecutionElementsStore::new)
.addStoreListener(storeListener);
}
@Override
public void executionEnded(final RobotTestsLaunch launch) {
// nothing to do
}
}
}