View Javadoc

1   /**
2    * Copyright (C) 2008 Atlassian
3    * 
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * 
8    *    http://www.apache.org/licenses/LICENSE-2.0
9    * 
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package com.atlassian.theplugin.idea.bamboo;
18  
19  import com.atlassian.theplugin.commons.bamboo.TestDetails;
20  import com.atlassian.theplugin.idea.Constants;
21  import com.atlassian.theplugin.util.ColorToHtml;
22  import com.intellij.execution.*;
23  import com.intellij.execution.configurations.ConfigurationFactory;
24  import com.intellij.execution.filters.TextConsoleBuilder;
25  import com.intellij.execution.filters.TextConsoleBuilderFactory;
26  import com.intellij.execution.impl.RunManagerImpl;
27  import com.intellij.execution.junit.JUnitConfiguration;
28  import com.intellij.execution.runners.JavaProgramRunner;
29  import com.intellij.execution.runners.RunStrategy;
30  import com.intellij.execution.ui.ConsoleView;
31  import com.intellij.execution.ui.ConsoleViewContentType;
32  import com.intellij.openapi.actionSystem.ActionGroup;
33  import com.intellij.openapi.actionSystem.ActionManager;
34  import com.intellij.openapi.actionSystem.ActionToolbar;
35  import com.intellij.openapi.actionSystem.AnActionEvent;
36  import com.intellij.openapi.project.Project;
37  import com.intellij.openapi.ui.Splitter;
38  import com.intellij.openapi.util.IconLoader;
39  import com.intellij.openapi.wm.ToolWindow;
40  import com.intellij.openapi.wm.ToolWindowAnchor;
41  import com.intellij.openapi.wm.ToolWindowManager;
42  import com.intellij.peer.PeerFactory;
43  import com.intellij.psi.PsiClass;
44  import com.intellij.psi.PsiManager;
45  import com.intellij.psi.PsiMethod;
46  import com.intellij.psi.search.GlobalSearchScope;
47  import com.intellij.ui.content.Content;
48  import com.intellij.util.ui.UIUtil;
49  
50  import javax.swing.*;
51  import javax.swing.event.TreeSelectionEvent;
52  import javax.swing.event.TreeSelectionListener;
53  import javax.swing.tree.*;
54  import java.awt.*;
55  import java.awt.event.MouseAdapter;
56  import java.awt.event.MouseEvent;
57  import java.util.HashMap;
58  import java.util.LinkedHashMap;
59  import java.util.List;
60  import java.util.Map;
61  
62  public final class TestResultsToolWindow {
63  	private final Project project;
64  
65  	public interface TestTree extends Expandable {
66  		boolean PASSED_TESTS_VISIBLE_DEFAULT = false;
67  		
68          void setPassedTestsVisible(boolean visible);
69  		boolean isPassedTestsVisible();
70  		boolean createTestConfiguration(JUnitConfiguration configuration);
71  	}
72  
73  	private static final String TOOL_WINDOW_TITLE = "Bamboo Failed Tests";
74  	private static final Icon TEST_PASSED_ICON = IconLoader.getIcon("/runConfigurations/testPassed.png");
75  	private static final Icon TEST_FAILED_ICON = IconLoader.getIcon("/runConfigurations/testFailed.png");
76  
77  	private HashMap<String, TestDetailsPanel> panelMap = new HashMap<String, TestDetailsPanel>();
78  
79  	public TestResultsToolWindow(Project project) {
80  		this.project = project;
81  	}
82  
83  
84  	public TestTree getTestTree(String name) {
85  		return panelMap.get(name);
86  	}
87  
88  	public void showTestResults(String buildKey, String buildNumber,
89                                  List<TestDetails> failedTests, List<TestDetails> succeededTests) {
90  		TestDetailsPanel detailsPanel;
91  		String contentKey = buildKey + "-" + buildNumber;
92  
93  		ToolWindowManager twm = ToolWindowManager.getInstance(project);
94  		ToolWindow testDetailsToolWindow = twm.getToolWindow(TOOL_WINDOW_TITLE);
95  		if (testDetailsToolWindow == null) {
96  			testDetailsToolWindow = twm.registerToolWindow(TOOL_WINDOW_TITLE, true, ToolWindowAnchor.BOTTOM);
97  			testDetailsToolWindow.setIcon(Constants.BAMBOO_TRACE_ICON);
98  		}
99  
100 		Content content = testDetailsToolWindow.getContentManager().findContent(contentKey);
101 
102 		if (content == null) {
103 			detailsPanel = new TestDetailsPanel(contentKey, failedTests, succeededTests);
104 			panelMap.remove(contentKey);
105 			panelMap.put(contentKey, detailsPanel);
106 
107 			PeerFactory peerFactory = PeerFactory.getInstance();
108 			content = peerFactory.getContentFactory().createContent(detailsPanel, contentKey, true);
109 			content.setIcon(Constants.BAMBOO_TRACE_ICON);
110 			content.putUserData(com.intellij.openapi.wm.ToolWindow.SHOW_CONTENT_ICON, Boolean.TRUE);
111 			testDetailsToolWindow.getContentManager().addContent(content);
112 		}
113 
114 		testDetailsToolWindow.getContentManager().setSelectedContent(content);
115 		testDetailsToolWindow.show(null);
116 	}
117 
118 	public void runFailedTests(AnActionEvent ev, boolean debug) {
119 		RunManagerImpl runManager = (RunManagerImpl) RunManager.getInstance(project);
120 		ConfigurationFactory factory = runManager.getFactory("JUnit", null);
121 		RunnerAndConfigurationSettings settings = runManager.createRunConfiguration("test from bamboo", factory);
122 		JUnitConfiguration conf = (JUnitConfiguration) settings.getConfiguration();
123 
124 		TestTree tree = getTestTree(ev.getPlace());
125 		if (tree == null) {
126 			return;
127 		}
128 		if (!tree.createTestConfiguration(conf)) {
129 			return;
130 		}
131 
132 		JavaProgramRunner runner;
133 		if (debug) {
134 			runner = ExecutionRegistry.getInstance().getDebuggerRunner();
135 		} else {
136 			runner = ExecutionRegistry.getInstance().getDefaultRunner();
137 		}
138 
139 		RunStrategy strategy = RunStrategy.getInstance();
140 		try {
141 			strategy.execute(settings, runner, ev.getDataContext());
142 		} catch (ExecutionException e) {
143 			e.printStackTrace();
144 		}
145 	}
146 
147 	public boolean canRunFailedTests(AnActionEvent ev) {
148 		TestTree tree = getTestTree(ev.getPlace());
149 		if (tree == null) {
150 			return false;
151 		}
152 		return tree.createTestConfiguration(null);
153 	}
154 
155 	private abstract class AbstractTreeNode extends DefaultMutableTreeNode {
156         public AbstractTreeNode(String s) {
157             super(s);
158         }
159 
160         public abstract void selected();
161         public abstract boolean isFailed();
162         public String getTestStats() {
163             return "";
164         }
165 
166         public abstract void navigate();
167 
168 		public abstract boolean createTestConfiguration(JUnitConfiguration configuration);
169     }
170 
171     private class TestDetailsPanel extends JPanel implements TestTree {
172 		private static final float SPLIT_RATIO = 0.3f;
173 
174 		private JTree tree;
175         private JScrollPane scroll;
176 
177         private List<TestDetails> succeededTests;
178         private List<TestDetails> failedTests;
179 
180         private ConsoleView console;
181 		private boolean passedTestsVisible;
182 
183 		private abstract class NonLeafNode extends AbstractTreeNode {
184 			protected int totalTests;
185 			protected int failedTests;
186 
187             public NonLeafNode(String s, int totalTests, int failedTests) {
188 				super(s);
189 				this.totalTests = totalTests;
190 				this.failedTests = failedTests;
191             }
192 
193 			@Override
194 			public void selected() {
195 				print("");
196 			}
197 
198             @Override
199 			public boolean isFailed() {
200                 return failedTests > 0;
201             }
202 
203 			@Override
204 			public String getTestStats() {
205 				return " (" + failedTests + " out of " + totalTests + " failed)";
206 			}
207 
208 			public void addFailedTest() {
209 				++failedTests;
210 			}
211 
212 			public void addTest() {
213 				++totalTests;
214 			}
215 
216 			public int getFailedTests() {
217 				return failedTests;
218 			}
219 
220 			public int getTotalTests() {
221 				return totalTests;
222 			}
223 		}
224 
225 		private class PackageNode extends NonLeafNode {
226 			public PackageNode(String s, int totalTests, int failedTests) {
227 				super(s, totalTests, failedTests);
228 			}
229 
230 			@Override
231 			public void navigate() {
232 				// no-op for packages
233 			}
234 
235 			public boolean createTestConfiguration(JUnitConfiguration configuration) {
236 				// bummer, JUnit does not support testing the whole package
237 				return false;
238 			}
239 		}
240 
241 		private class ClassNode extends NonLeafNode {
242 			private String className;
243 
244 			public ClassNode(String fqcn, int totalTests, int failedTests) {
245 				super(fqcn.substring(fqcn.lastIndexOf('.') + 1), totalTests, failedTests);
246 				className = fqcn;
247 			}
248 
249 			@Override
250 			public void navigate() {
251 				PsiClass cls = PsiManager.getInstance(project).findClass(className,
252 						GlobalSearchScope.allScope(project));
253 				if (cls != null) {
254 					cls.navigate(true);
255 				}
256 			}
257 
258 			public boolean createTestConfiguration(JUnitConfiguration configuration) {
259 				PsiManager mgr = PsiManager.getInstance(project);
260 
261 				PsiClass cls = mgr.findClass(className, GlobalSearchScope.allScope(project));
262 				if (cls == null) {
263 					return false;
264 				}
265 				if (configuration != null) {
266 					configuration.beClassConfiguration(cls);
267 				}
268 				return true;
269 			}
270 		}
271 
272 		private abstract class TestNode extends AbstractTreeNode {
273 			protected TestDetails details;
274 
275 			public TestNode(TestDetails details) {
276 				super(details.getTestMethodName());
277 				this.details = details;
278 			}
279 
280 			private PsiMethod getMethod() {
281 				PsiClass cls = PsiManager.getInstance(project).findClass(details.getTestClassName(),
282 						GlobalSearchScope.allScope(project));
283 				if (cls == null) {
284 					return null;
285 				}
286 				PsiMethod[] methods = cls.findMethodsByName(details.getTestMethodName(), false);
287 				if (methods.length == 0 || methods[0] == null) {
288 					return null;
289 				}
290 
291 				return methods[0];
292 			}
293 
294 			@Override
295 			public void navigate() {
296 				PsiMethod m = getMethod();
297 				if (m != null) {
298 					m.navigate(true);
299 				}
300 			}
301 
302 			public boolean createTestConfiguration(JUnitConfiguration configuration) {
303 				PsiMethod m = getMethod();
304 
305 				if (m != null) {
306 					if (configuration != null) {
307 						configuration.beMethodConfiguration(PsiLocation.fromPsiElement(project, m));
308 					}
309 					return true;
310 				}
311 				return false;
312 			}
313 		}
314 
315 		private class TestErrorInfoNode extends TestNode {
316 			public TestErrorInfoNode(TestDetails details) {
317 				super(details);
318 			}
319 
320 			@Override
321 			public void selected() {
322 				print(details.getErrors());
323 			}
324 
325             @Override
326 			public boolean isFailed() {
327                 return true;
328             }
329 		}
330 
331         private class TestSuccessInfoNode extends TestNode {
332             public TestSuccessInfoNode(TestDetails details) {
333                 super(details);
334             }
335 
336             @Override
337 			public void selected() {
338                 print("Test successful");
339             }
340 
341             @Override
342 			public boolean isFailed() {
343                 return false;
344             }
345 		}
346 
347 		private JTree createTestTree() {
348 			NonLeafNode root = new PackageNode("All Tests",
349 					failedTests.size() + succeededTests.size(), failedTests.size());
350 
351             Map<String, NonLeafNode> packages = new LinkedHashMap<String, NonLeafNode>();
352 			for (TestDetails d : succeededTests) {
353 				String fqcn = d.getTestClassName();
354 				String pkg = fqcn.substring(0, fqcn.lastIndexOf('.'));
355 				NonLeafNode n = packages.get(pkg);
356 				if (n == null) {
357 					packages.put(pkg, new PackageNode(pkg, 1, 0));
358 				} else {
359 					n.addTest();
360 				}
361             }
362             for (TestDetails d : failedTests) {
363                 String fqcn = d.getTestClassName();
364                 String pkg = fqcn.substring(0, fqcn.lastIndexOf('.'));
365 				NonLeafNode n = packages.get(pkg);
366 				if (n == null) {
367 	                packages.put(pkg, new PackageNode(pkg, 1, 1));
368 				} else {
369 					n.addTest();
370 					n.addFailedTest();
371 				}
372 			}
373 
374             for (Map.Entry<String, NonLeafNode> p : packages.entrySet()) {
375 				NonLeafNode n = p.getValue();
376 				if (n.isFailed() || passedTestsVisible) {
377 					root.add(n);
378 				}
379 			}
380 
381 			Map<String, NonLeafNode> classes = new LinkedHashMap<String, NonLeafNode>();
382 			for (TestDetails d : succeededTests) {
383 				String fqcn = d.getTestClassName();
384 				NonLeafNode n = classes.get(fqcn);
385 				if (n == null) {
386 					classes.put(fqcn, new ClassNode(fqcn, 1, 0));
387 				} else {
388 					n.addTest();
389 				}
390 			}
391             for (TestDetails d : failedTests) {
392                 String fqcn = d.getTestClassName();
393 				NonLeafNode n = classes.get(fqcn);
394 				if (n == null) {
395 					classes.put(fqcn, new ClassNode(fqcn, 1, 1));
396 				} else {
397 					n.addTest();
398 					n.addFailedTest();
399 				}
400 			}
401 
402             for (Map.Entry<String, NonLeafNode> c : classes.entrySet()) {
403 				String fqcn = c.getKey();
404 				String pkg = fqcn.substring(0, fqcn.lastIndexOf('.'));
405 				NonLeafNode n = c.getValue(); packages.get(pkg);
406 				if (n.isFailed() || passedTestsVisible) {
407 					packages.get(pkg).add(n);
408 				}
409 			}
410 
411             if (passedTestsVisible) {
412                 for (TestDetails d : succeededTests) {
413                     String fqcn = d.getTestClassName();
414                     AbstractTreeNode node = classes.get(fqcn);
415                     node.add(new TestSuccessInfoNode(d));
416                 }
417             }
418             for (TestDetails d : failedTests) {
419                 String fqcn = d.getTestClassName();
420                 AbstractTreeNode node = classes.get(fqcn);
421                 node.add(new TestErrorInfoNode(d));
422             }
423 
424             DefaultTreeModel tm = new DefaultTreeModel(root);
425 			JTree testTree = new JTree(tm);
426 			testTree.setRootVisible(true);
427 			testTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
428 			testTree.addTreeSelectionListener(new TreeSelectionListener() {
429 				public void valueChanged(TreeSelectionEvent e) {
430 					AbstractTreeNode errInfoNode = (AbstractTreeNode) e.getPath().getLastPathComponent();
431 					errInfoNode.selected();
432 				}
433 			});
434 			testTree.addMouseListener(new MouseAdapter() {
435 
436 				@Override
437 				public void mouseClicked(MouseEvent e) {
438 					if (e.getClickCount() >= 2) {
439 						TreePath path = tree.getPathForLocation(e.getX(), e.getY());
440 						AbstractTreeNode node = (AbstractTreeNode) path.getLastPathComponent();
441 						node.navigate();
442 					}
443 				}
444 			});
445 
446 			DefaultTreeCellRenderer renderer = new MyDefaultTreeCellRenderer();
447             testTree.setCellRenderer(renderer);
448 
449 			return testTree;
450 		}
451 
452 		public TestDetailsPanel(String name, final List<TestDetails> failedTests,
453                                 final List<TestDetails> succeededTests) {
454             this.failedTests = failedTests;
455             this.succeededTests = succeededTests;
456 
457             if (failedTests.size() > 0) {
458 
459 				Splitter split = new Splitter(false, SPLIT_RATIO);
460 				split.setShowDividerControls(true);
461 
462 				setLayout(new GridBagLayout());
463 				GridBagConstraints gbc = new GridBagConstraints();
464 				gbc.gridx = 0;
465 				gbc.gridy = 0;
466 				gbc.weightx = 1.0;
467 				gbc.weighty = 1.0;
468 				gbc.fill = GridBagConstraints.BOTH;
469 
470 				JPanel treePanel = new JPanel();
471 				treePanel.setLayout(new GridBagLayout());
472 				GridBagConstraints gbc1 = new GridBagConstraints();
473 
474 				ActionManager manager = ActionManager.getInstance();
475 				ActionGroup group = (ActionGroup) manager.getAction("ThePlugin.Bamboo.TestResultsToolBar");
476 				ActionToolbar toolbar = manager.createActionToolbar(name, group, true);
477 				JComponent comp = toolbar.getComponent();
478 
479 				gbc1.gridx = 0;
480 				gbc1.gridy = 0;
481 				gbc1.weightx = 1.0;
482 				gbc1.weighty = 0.0;
483 				gbc1.fill = GridBagConstraints.HORIZONTAL;
484 				
485 				treePanel.add(comp, gbc1);
486 
487 				passedTestsVisible = PASSED_TESTS_VISIBLE_DEFAULT;
488 
489 				tree = createTestTree();
490 				expand();
491 				scroll = new JScrollPane(tree);
492 
493 				gbc1.gridy = 1;
494 				gbc1.weighty = 1.0;
495 				gbc1.fill = GridBagConstraints.BOTH;
496 
497                 treePanel.add(scroll, gbc1);
498 
499 				split.setFirstComponent(treePanel);
500 
501 				JPanel consolePanel = new JPanel();
502 				consolePanel.setLayout(new GridBagLayout());
503 
504 				gbc1.gridy = 0;
505 				gbc1.weighty = 0.0;
506 				gbc1.fill = GridBagConstraints.NONE;
507 				gbc1.anchor = GridBagConstraints.LINE_START;
508 
509 				JLabel label = new JLabel("Test Stack Trace");
510 
511 				Dimension d = label.getPreferredSize();
512 				d.height = toolbar.getMaxButtonHeight();
513 				label.setMinimumSize(d);
514 				label.setPreferredSize(d); 
515 				consolePanel.add(label, gbc1);
516 
517 				TextConsoleBuilderFactory factory = TextConsoleBuilderFactory.getInstance();
518 				TextConsoleBuilder builder = factory.createBuilder(project);
519 				console = builder.getConsole();
520 
521 				gbc1.gridy = 1;
522 				gbc1.weighty = 1.0;
523 				gbc1.fill = GridBagConstraints.BOTH;
524 
525 				consolePanel.add(console.getComponent(), gbc1);
526 
527 				split.setSecondComponent(consolePanel);
528 				
529 				add(split, gbc);
530 			} else {
531 				add(new JLabel("No failed failedTests in build " + name));
532 			}
533 		}
534 
535 		public void print(String txt) {
536 			console.clear();
537 			console.print(txt, ConsoleViewContentType.ERROR_OUTPUT);
538 		}
539 
540 		public void expand() {
541 			for (int row = 1; row < tree.getRowCount(); ++row) {
542 				tree.expandRow(row);
543 			}
544 		}
545 
546 		public void collapse() {
547 			for (int row = tree.getRowCount() - 1; row > 0; --row) {
548 				tree.collapseRow(row);
549 			}
550 		}
551 
552         public void setPassedTestsVisible(boolean visible) {
553 			passedTestsVisible = visible;
554 			tree = createTestTree();
555 			expand();
556 			scroll.setViewportView(tree);
557         }
558 
559 		public boolean isPassedTestsVisible() {
560 			return passedTestsVisible;
561 		}
562 
563 	    public boolean createTestConfiguration(JUnitConfiguration configuration) {
564 		    TreePath p = tree.getSelectionPath();
565 		    return p != null && ((AbstractTreeNode) p.getLastPathComponent()).createTestConfiguration(configuration);
566 	    }
567     }
568 
569     private static class MyDefaultTreeCellRenderer extends DefaultTreeCellRenderer {
570         @Override
571             public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected,
572                 boolean expanded, boolean leaf, int row, boolean hasFocus) {
573 
574             Component c = super.getTreeCellRendererComponent(
575                     tree, value, selected, expanded, leaf, row, hasFocus);
576 
577             // this sort of is not right, as it assumes that getTreeCellRendererComponent() of the
578             // DefaultTreeCellRenderer will always return _this_ (JLabel). If the implementation changes
579             // someday, we are screwed :)
580             try {
581                 AbstractTreeNode node = (AbstractTreeNode) value;
582                 if (node.isFailed()) {
583                     setIcon(TEST_FAILED_ICON);
584                 } else {
585                     setIcon(TEST_PASSED_ICON);
586                 }
587                 Color statsColor = selected
588                         ? UIUtil.getTreeSelectionForeground() : UIUtil.getInactiveTextColor();
589                 StringBuilder txt = new StringBuilder();
590                 txt.append("<html><body>");
591                 txt.append(getText());
592                 txt.append(" <font color=");
593                 txt.append(ColorToHtml.getHtmlFromColor(statsColor));
594                 txt.append("><i>");
595                 txt.append(node.getTestStats());
596                 txt.append("</i></font>");
597                 txt.append("</body></html>");
598                 setText(txt.toString());
599             } catch (ClassCastException e) {
600                 // should not happen, making compiler happy
601                 setIcon(null);
602             }
603 
604             return c;
605         }
606     }
607 
608 }