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.filters.TextConsoleBuilder;
23  import com.intellij.execution.filters.TextConsoleBuilderFactory;
24  import com.intellij.execution.ui.ConsoleView;
25  import com.intellij.execution.ui.ConsoleViewContentType;
26  import com.intellij.openapi.actionSystem.ActionGroup;
27  import com.intellij.openapi.actionSystem.ActionManager;
28  import com.intellij.openapi.actionSystem.ActionToolbar;
29  import com.intellij.openapi.project.Project;
30  import com.intellij.openapi.ui.Splitter;
31  import com.intellij.openapi.util.IconLoader;
32  import com.intellij.openapi.wm.ToolWindow;
33  import com.intellij.openapi.wm.ToolWindowAnchor;
34  import com.intellij.openapi.wm.ToolWindowManager;
35  import com.intellij.peer.PeerFactory;
36  import com.intellij.psi.PsiClass;
37  import com.intellij.psi.PsiManager;
38  import com.intellij.psi.PsiMethod;
39  import com.intellij.psi.search.GlobalSearchScope;
40  import com.intellij.ui.content.Content;
41  import com.intellij.util.ui.UIUtil;
42  
43  import javax.swing.*;
44  import javax.swing.event.TreeSelectionEvent;
45  import javax.swing.event.TreeSelectionListener;
46  import javax.swing.tree.DefaultMutableTreeNode;
47  import javax.swing.tree.DefaultTreeCellRenderer;
48  import javax.swing.tree.DefaultTreeModel;
49  import javax.swing.tree.TreePath;
50  import javax.swing.tree.TreeSelectionModel;
51  import java.awt.*;
52  import java.awt.event.MouseAdapter;
53  import java.awt.event.MouseEvent;
54  import java.util.HashMap;
55  import java.util.LinkedHashMap;
56  import java.util.List;
57  import java.util.Map;
58  
59  public final class TestResultsToolWindow {
60  	private final Project project;
61  
62  	public interface TestTree extends Expandable {
63  		boolean PASSED_TESTS_VISIBLE_DEFAULT = false;
64  		
65          void setPassedTestsVisible(boolean visible);
66  		boolean isPassedTestsVisible();
67  	}
68  
69  	private static final String TOOL_WINDOW_TITLE = "Bamboo Failed Tests";
70  	private static final Icon TEST_PASSED_ICON = IconLoader.getIcon("/runConfigurations/testPassed.png");
71  	private static final Icon TEST_FAILED_ICON = IconLoader.getIcon("/runConfigurations/testFailed.png");
72  
73  	private HashMap<String, TestDetailsPanel> panelMap = new HashMap<String, TestDetailsPanel>();
74  
75  	public TestResultsToolWindow(Project project) {
76  		this.project = project;
77  	}
78  
79  
80  	public TestTree getTestTree(String name) {
81  		return panelMap.get(name);
82  	}
83  
84  	public void showTestResults(String buildKey, String buildNumber,
85                                  List<TestDetails> failedTests, List<TestDetails> succeededTests) {
86  		TestDetailsPanel detailsPanel;
87  		String contentKey = buildKey + "-" + buildNumber;
88  
89  		ToolWindowManager twm = ToolWindowManager.getInstance(project);
90  		ToolWindow testDetailsToolWindow = twm.getToolWindow(TOOL_WINDOW_TITLE);
91  		if (testDetailsToolWindow == null) {
92  			testDetailsToolWindow = twm.registerToolWindow(TOOL_WINDOW_TITLE, true, ToolWindowAnchor.BOTTOM);
93  			testDetailsToolWindow.setIcon(Constants.BAMBOO_TRACE_ICON);
94  		}
95  
96  		Content content = testDetailsToolWindow.getContentManager().findContent(contentKey);
97  
98  		if (content == null) {
99  			detailsPanel = new TestDetailsPanel(contentKey, failedTests, succeededTests);
100 			panelMap.remove(contentKey);
101 			panelMap.put(contentKey, detailsPanel);
102 
103 			PeerFactory peerFactory = PeerFactory.getInstance();
104 			content = peerFactory.getContentFactory().createContent(detailsPanel, contentKey, true);
105 			content.setIcon(Constants.BAMBOO_TRACE_ICON);
106 			content.putUserData(com.intellij.openapi.wm.ToolWindow.SHOW_CONTENT_ICON, Boolean.TRUE);
107 			testDetailsToolWindow.getContentManager().addContent(content);
108 		}
109 
110 		testDetailsToolWindow.getContentManager().setSelectedContent(content);
111 		testDetailsToolWindow.show(null);
112 	}
113 
114     private abstract class AbstractTreeNode extends DefaultMutableTreeNode {
115         public AbstractTreeNode(String s) {
116             super(s);
117         }
118 
119         public abstract void selected();
120         public abstract boolean isFailed();
121         public String getTestStats() {
122             return "";
123         }
124 
125         public abstract void navigate();
126     }
127 
128     private class TestDetailsPanel extends JPanel implements TestTree {
129 		private static final float SPLIT_RATIO = 0.3f;
130 
131 		private JTree tree;
132         private JScrollPane scroll;
133 
134         private List<TestDetails> succeededTests;
135         private List<TestDetails> failedTests;
136 
137         private ConsoleView console;
138 		private boolean passedTestsVisible;
139 
140 		private abstract class NonLeafNode extends AbstractTreeNode {
141 			protected int totalTests;
142 			protected int failedTests;
143 
144             public NonLeafNode(String s, int totalTests, int failedTests) {
145 				super(s);
146 				this.totalTests = totalTests;
147 				this.failedTests = failedTests;
148             }
149 
150 			@Override
151 			public void selected() {
152 				print("");
153 			}
154 
155             @Override
156 			public boolean isFailed() {
157                 return failedTests > 0;
158             }
159 
160 			@Override
161 			public String getTestStats() {
162 				return " (" + failedTests + " out of " + totalTests + " failed)";
163 			}
164 
165 			public void addFailedTest() {
166 				++failedTests;
167 			}
168 
169 			public void addTest() {
170 				++totalTests;
171 			}
172 
173 			public int getFailedTests() {
174 				return failedTests;
175 			}
176 
177 			public int getTotalTests() {
178 				return totalTests;
179 			}
180 		}
181 
182 		private class PackageNode extends NonLeafNode {
183 			public PackageNode(String s, int totalTests, int failedTests) {
184 				super(s, totalTests, failedTests);
185 			}
186 
187 			@Override
188 			public void navigate() {
189 				// no-op for packages
190 			}
191 
192 		}
193 
194 		private class ClassNode extends NonLeafNode {
195 			private String className;
196 
197 			public ClassNode(String fqcn, int totalTests, int failedTests) {
198 				super(fqcn.substring(fqcn.lastIndexOf('.') + 1), totalTests, failedTests);
199 				className = fqcn;
200 			}
201 
202 			@Override
203 			public void navigate() {
204 				PsiClass cls = PsiManager.getInstance(project).findClass(className,
205 						GlobalSearchScope.allScope(project));
206 				if (cls != null) {
207 					cls.navigate(true);
208 				}
209 			}
210 		}
211 
212 		private abstract class TestNode extends AbstractTreeNode {
213 			protected TestDetails details;
214 
215 			public TestNode(TestDetails details) {
216 				super(details.getTestMethodName());
217 				this.details = details;
218 			}
219 
220 			@Override
221 			public void navigate() {
222 				PsiClass cls = PsiManager.getInstance(project).findClass(details.getTestClassName(),
223 						GlobalSearchScope.allScope(project));
224 				if (cls == null) {
225 					return;
226 				}
227 				PsiMethod[] methods = cls.findMethodsByName(details.getTestMethodName(), false);
228 				if (methods == null || methods.length == 0 && methods[0] == null) {
229 					return;
230 				}
231 				methods[0].navigate(true);
232 			}
233 		}
234 		private class TestErrorInfoNode extends TestNode {
235 			public TestErrorInfoNode(TestDetails details) {
236 				super(details);
237 			}
238 
239 			@Override
240 			public void selected() {
241 				print(details.getErrors());
242 			}
243 
244             @Override
245 			public boolean isFailed() {
246                 return true;
247             }
248 		}
249 
250         private class TestSuccessInfoNode extends TestNode {
251             public TestSuccessInfoNode(TestDetails details) {
252                 super(details);
253             }
254 
255             @Override
256 			public void selected() {
257                 print("Test successful");
258             }
259 
260             @Override
261 			public boolean isFailed() {
262                 return false;
263             }
264 		}
265 
266 		private JTree createTestTree() {
267 			NonLeafNode root = new PackageNode("All Tests",
268 					failedTests.size() + succeededTests.size(), failedTests.size());
269 
270             Map<String, NonLeafNode> packages = new LinkedHashMap<String, NonLeafNode>();
271 			for (TestDetails d : succeededTests) {
272 				String fqcn = d.getTestClassName();
273 				String pkg = fqcn.substring(0, fqcn.lastIndexOf('.'));
274 				NonLeafNode n = packages.get(pkg);
275 				if (n == null) {
276 					packages.put(pkg, new PackageNode(pkg, 1, 0));
277 				} else {
278 					n.addTest();
279 				}
280             }
281             for (TestDetails d : failedTests) {
282                 String fqcn = d.getTestClassName();
283                 String pkg = fqcn.substring(0, fqcn.lastIndexOf('.'));
284 				NonLeafNode n = packages.get(pkg);
285 				if (n == null) {
286 	                packages.put(pkg, new PackageNode(pkg, 1, 1));
287 				} else {
288 					n.addTest();
289 					n.addFailedTest();
290 				}
291 			}
292 
293             for (Map.Entry<String, NonLeafNode> p : packages.entrySet()) {
294 				NonLeafNode n = p.getValue();
295 				if (n.isFailed() || passedTestsVisible) {
296 					root.add(n);
297 				}
298 			}
299 
300 			Map<String, NonLeafNode> classes = new LinkedHashMap<String, NonLeafNode>();
301 			for (TestDetails d : succeededTests) {
302 				String fqcn = d.getTestClassName();
303 				NonLeafNode n = classes.get(fqcn);
304 				if (n == null) {
305 					classes.put(fqcn, new ClassNode(fqcn, 1, 0));
306 				} else {
307 					n.addTest();
308 				}
309 			}
310             for (TestDetails d : failedTests) {
311                 String fqcn = d.getTestClassName();
312 				NonLeafNode n = classes.get(fqcn);
313 				if (n == null) {
314 					classes.put(fqcn, new ClassNode(fqcn, 1, 1));
315 				} else {
316 					n.addTest();
317 					n.addFailedTest();
318 				}
319 			}
320 
321             for (Map.Entry<String, NonLeafNode> c : classes.entrySet()) {
322 				String fqcn = c.getKey();
323 				String pkg = fqcn.substring(0, fqcn.lastIndexOf('.'));
324 				NonLeafNode n = c.getValue(); packages.get(pkg);
325 				if (n.isFailed() || passedTestsVisible) {
326 					packages.get(pkg).add(n);
327 				}
328 			}
329 
330             if (passedTestsVisible) {
331                 for (TestDetails d : succeededTests) {
332                     String fqcn = d.getTestClassName();
333                     AbstractTreeNode node = classes.get(fqcn);
334                     node.add(new TestSuccessInfoNode(d));
335                 }
336             }
337             for (TestDetails d : failedTests) {
338                 String fqcn = d.getTestClassName();
339                 AbstractTreeNode node = classes.get(fqcn);
340                 node.add(new TestErrorInfoNode(d));
341             }
342 
343             DefaultTreeModel tm = new DefaultTreeModel(root);
344 			JTree testTree = new JTree(tm);
345 			testTree.setRootVisible(true);
346 			testTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
347 			testTree.addTreeSelectionListener(new TreeSelectionListener() {
348 				public void valueChanged(TreeSelectionEvent e) {
349 					AbstractTreeNode errInfoNode = (AbstractTreeNode) e.getPath().getLastPathComponent();
350 					errInfoNode.selected();
351 				}
352 			});
353 			testTree.addMouseListener(new MouseAdapter() {
354 
355 				@Override
356 				public void mouseClicked(MouseEvent e) {
357 					if (e.getClickCount() >= 2) {
358 						TreePath path = tree.getPathForLocation(e.getX(), e.getY());
359 						AbstractTreeNode node = (AbstractTreeNode) path.getLastPathComponent();
360 						node.navigate();
361 					}
362 				}
363 			});
364 
365 			DefaultTreeCellRenderer renderer = new MyDefaultTreeCellRenderer();
366             testTree.setCellRenderer(renderer);
367 
368 			return testTree;
369 		}
370 
371 		public TestDetailsPanel(String name, final List<TestDetails> failedTests,
372                                 final List<TestDetails> succeededTests) {
373             this.failedTests = failedTests;
374             this.succeededTests = succeededTests;
375 
376             if (failedTests.size() > 0) {
377 
378 				Splitter split = new Splitter(false, SPLIT_RATIO);
379 				split.setShowDividerControls(true);
380 
381 				setLayout(new GridBagLayout());
382 				GridBagConstraints gbc = new GridBagConstraints();
383 				gbc.gridx = 0;
384 				gbc.gridy = 0;
385 				gbc.weightx = 1.0;
386 				gbc.weighty = 1.0;
387 				gbc.fill = GridBagConstraints.BOTH;
388 
389 				JPanel treePanel = new JPanel();
390 				treePanel.setLayout(new GridBagLayout());
391 				GridBagConstraints gbc1 = new GridBagConstraints();
392 
393 				ActionManager manager = ActionManager.getInstance();
394 				ActionGroup group = (ActionGroup) manager.getAction("ThePlugin.Bamboo.TestResultsToolBar");
395 				ActionToolbar toolbar = manager.createActionToolbar(name, group, true);
396 				JComponent comp = toolbar.getComponent();
397 
398 				gbc1.gridx = 0;
399 				gbc1.gridy = 0;
400 				gbc1.weightx = 1.0;
401 				gbc1.weighty = 0.0;
402 				gbc1.fill = GridBagConstraints.HORIZONTAL;
403 				
404 				treePanel.add(comp, gbc1);
405 
406 				passedTestsVisible = PASSED_TESTS_VISIBLE_DEFAULT;
407 
408 				tree = createTestTree();
409 				expand();
410 				scroll = new JScrollPane(tree);
411 
412 				gbc1.gridy = 1;
413 				gbc1.weighty = 1.0;
414 				gbc1.fill = GridBagConstraints.BOTH;
415 
416                 treePanel.add(scroll, gbc1);
417 
418 				split.setFirstComponent(treePanel);
419 
420 				JPanel consolePanel = new JPanel();
421 				consolePanel.setLayout(new GridBagLayout());
422 
423 				gbc1.gridy = 0;
424 				gbc1.weighty = 0.0;
425 				gbc1.fill = GridBagConstraints.NONE;
426 				gbc1.anchor = GridBagConstraints.LINE_START;
427 
428 				JLabel label = new JLabel("Test Stack Trace");
429 
430 				Dimension d = label.getPreferredSize();
431 				d.height = toolbar.getMaxButtonHeight();
432 				label.setMinimumSize(d);
433 				label.setPreferredSize(d); 
434 				consolePanel.add(label, gbc1);
435 
436 				TextConsoleBuilderFactory factory = TextConsoleBuilderFactory.getInstance();
437 				TextConsoleBuilder builder = factory.createBuilder(project);
438 				console = builder.getConsole();
439 
440 				gbc1.gridy = 1;
441 				gbc1.weighty = 1.0;
442 				gbc1.fill = GridBagConstraints.BOTH;
443 
444 				consolePanel.add(console.getComponent(), gbc1);
445 
446 				split.setSecondComponent(consolePanel);
447 				
448 				add(split, gbc);
449 			} else {
450 				add(new JLabel("No failed failedTests in build " + name));
451 			}
452 		}
453 
454 		public void print(String txt) {
455 			console.clear();
456 			console.print(txt, ConsoleViewContentType.NORMAL_OUTPUT);
457 		}
458 
459 		public void expand() {
460 			for (int row = 1; row < tree.getRowCount(); ++row) {
461 				tree.expandRow(row);
462 			}
463 		}
464 
465 		public void collapse() {
466 			for (int row = tree.getRowCount() - 1; row > 0; --row) {
467 				tree.collapseRow(row);
468 			}
469 		}
470 
471         public void setPassedTestsVisible(boolean visible) {
472 			passedTestsVisible = visible;
473 			tree = createTestTree();
474 			expand();
475 			scroll.setViewportView(tree);
476         }
477 
478 		public boolean isPassedTestsVisible() {
479 			return passedTestsVisible;
480 		}
481 
482     }
483 
484     private static class MyDefaultTreeCellRenderer extends DefaultTreeCellRenderer {
485         @Override
486             public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected,
487                 boolean expanded, boolean leaf, int row, boolean hasFocus) {
488 
489             Component c = super.getTreeCellRendererComponent(
490                     tree, value, selected, expanded, leaf, row, hasFocus);
491 
492             // this sort of is not right, as it assumes that getTreeCellRendererComponent() of the
493             // DefaultTreeCellRenderer will always return _this_ (JLabel). If the implementation changes
494             // someday, we are screwed :)
495             try {
496                 AbstractTreeNode node = (AbstractTreeNode) value;
497                 if (node.isFailed()) {
498                     setIcon(TEST_FAILED_ICON);
499                 } else {
500                     setIcon(TEST_PASSED_ICON);
501                 }
502                 Color statsColor = selected
503                         ? UIUtil.getTreeSelectionForeground() : UIUtil.getInactiveTextColor();
504                 StringBuilder txt = new StringBuilder();
505                 txt.append("<html><body>");
506                 txt.append(getText());
507                 txt.append(" <font color=");
508                 txt.append(ColorToHtml.getHtmlFromColor(statsColor));
509                 txt.append("><i>");
510                 txt.append(node.getTestStats());
511                 txt.append("</i></font>");
512                 txt.append("</body></html>");
513                 setText(txt.toString());
514             } catch (ClassCastException e) {
515                 // should not happen, making compiler happy
516                 setIcon(null);
517             }
518 
519             return c;
520         }
521     }
522 
523 }