1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
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
493
494
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
516 setIcon(null);
517 }
518
519 return c;
520 }
521 }
522
523 }