Merge branch 'master' of git.labs.intellij.net:idea/community
[idea/community.git] / platform / smRunner / src / com / intellij / execution / testframework / sm / runner / SMTestProxy.java
1 /*
2  * Copyright 2000-2009 JetBrains s.r.o.
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 package com.intellij.execution.testframework.sm.runner;
17
18 import com.intellij.execution.Location;
19 import com.intellij.execution.testframework.*;
20 import com.intellij.execution.testframework.sm.TestsLocationProviderUtil;
21 import com.intellij.execution.testframework.sm.runner.states.*;
22 import com.intellij.execution.testframework.sm.runner.ui.TestsPresentationUtil;
23 import com.intellij.execution.ui.ConsoleViewContentType;
24 import com.intellij.ide.util.EditSourceUtil;
25 import com.intellij.openapi.diagnostic.Logger;
26 import com.intellij.openapi.extensions.Extensions;
27 import com.intellij.openapi.project.Project;
28 import com.intellij.openapi.util.Key;
29 import com.intellij.pom.Navigatable;
30 import com.intellij.testIntegration.TestLocationProvider;
31 import org.jetbrains.annotations.NotNull;
32 import org.jetbrains.annotations.Nullable;
33
34 import java.util.ArrayList;
35 import java.util.Collections;
36 import java.util.List;
37
38 /**
39  * @author: Roman Chernyatchik
40  */
41 public class SMTestProxy extends AbstractTestProxy {
42   private static final Logger LOG = Logger.getInstance(SMTestProxy.class.getName());
43
44   private List<SMTestProxy> myChildren;
45   private SMTestProxy myParent;
46
47   private AbstractState myState = NotRunState.getInstance();
48   private final String myName;
49   private Integer myDuration = null; // duration is unknown
50   @Nullable private final String myLocationUrl;
51   private boolean myDurationIsCached = false; // is used for separating unknown and unset duration
52   private boolean myHasErrors = false;
53   private boolean myHasErrorsCached = false;
54
55   private final boolean myIsSuite;
56   private boolean myIsEmptyIsCached = false; // is used for separating unknown and unset values
57   private boolean myIsEmpty = true;
58
59   public SMTestProxy(final String testName, final boolean isSuite,
60                      @Nullable final String locationUrl) {
61     myName = testName;
62     myIsSuite = isSuite;
63     myLocationUrl = locationUrl;
64   }
65
66   public boolean isInProgress() {
67     //final SMTestProxy parent = getParent();
68
69     return myState.isInProgress();
70   }
71
72   public boolean isDefect() {
73     return myState.isDefect();
74   }
75
76   public boolean shouldRun() {
77     return true;
78   }
79
80   public int getMagnitude() {
81     // Is used by some of Tests Filters
82
83     //WARN: It is Hack, see PoolOfTestStates, API is necessary
84     return getMagnitudeInfo().getValue();
85   }
86
87   public TestStateInfo.Magnitude getMagnitudeInfo() {
88     return myState.getMagnitude();
89   }
90
91   public boolean hasErrors() {
92     // if already cached
93     if (myHasErrorsCached) {
94       return myHasErrors;
95     }
96
97     final boolean canCacheErrors = !myState.isInProgress();
98     // calculate
99     final boolean hasErrors = calcHasErrors();
100     if (canCacheErrors) {
101       myHasErrors = hasErrors;
102       myHasErrorsCached = true;
103     }
104     return hasErrors;
105   }
106
107   private boolean calcHasErrors() {
108     if (myHasErrors) {
109       return true;
110     }
111
112     for (SMTestProxy child : getChildren()) {
113       if (child.hasErrors()) {
114         return true;
115       }
116     }
117     return false;
118   }
119
120   public boolean isLeaf() {
121     return myChildren == null || myChildren.isEmpty();
122   }
123
124   @Override
125   public boolean isInterrupted() {
126     return myState.wasTerminated();
127   }
128
129   public boolean isPassed() {
130     return myState.getMagnitude() == TestStateInfo.Magnitude.SKIPPED_INDEX ||
131            myState.getMagnitude() == TestStateInfo.Magnitude.COMPLETE_INDEX ||
132            myState.getMagnitude() == TestStateInfo.Magnitude.PASSED_INDEX; 
133   }
134
135   public void addChild(final SMTestProxy child) {
136     if (myChildren == null) {
137       myChildren = new ArrayList<SMTestProxy>();
138     }
139     myChildren.add(child);
140
141     // add printable
142     //
143     // add link to child's future output in correct place
144     // actually if after this suite will obtain output
145     // it will place it after this child and before future child
146     addLast(child);
147
148     // add child
149     //
150     //TODO reset children cache
151     child.setParent(this);
152     // if parent is being printed then all childs output
153     // should be also send to the same printer
154     child.setPrinter(myPrinter);
155   }
156
157   public String getName() {
158     return myName;
159   }
160
161   @Nullable
162   public Location getLocation(final Project project) {
163     //determines location of test proxy
164
165     //TODO multiresolve support
166
167     if (myLocationUrl == null) {
168       return null;
169     }
170
171     final String protocolId = TestsLocationProviderUtil.extractProtocol(myLocationUrl);
172     final String path = TestsLocationProviderUtil.extractPath(myLocationUrl);
173
174     if (protocolId != null && path != null) {
175       for (TestLocationProvider provider : Extensions.getExtensions(TestLocationProvider.EP_NAME)) {
176         final List<Location> locations = provider.getLocation(protocolId, path, project);
177         if (!locations.isEmpty()) {
178           return locations.iterator().next();
179         }
180       }
181     }
182
183     return null;
184   }
185
186   @Nullable
187   public Navigatable getDescriptor(final Location location) {
188     // by location gets navigatable element.
189     // It can be file or place in file (e.g. when OPEN_FAILURE_LINE is enabled)
190
191     if (location != null) {
192       return EditSourceUtil.getDescriptor(location.getPsiElement());
193     }
194     return null;
195   }
196
197   public boolean isSuite() {
198     return myIsSuite;
199   }
200
201   public SMTestProxy getParent() {
202     return myParent;
203   }
204
205   public List<? extends SMTestProxy> getChildren() {
206     return myChildren != null ? myChildren : Collections.<SMTestProxy>emptyList();
207   }
208
209   public List<SMTestProxy> getAllTests() {
210     final List<SMTestProxy> allTests = new ArrayList<SMTestProxy>();
211
212     allTests.add(this);
213
214     for (SMTestProxy child : getChildren()) {
215       allTests.addAll(child.getAllTests());
216     }
217
218     return allTests;
219   }
220
221
222   public void setStarted() {
223     myState = !myIsSuite ? TestInProgressState.TEST : new SuiteInProgressState(this);
224   }
225
226   /**
227    * Calculates and caches duration of test or suite
228    * @return null if duration is unknown, otherwise duration value in milliseconds;
229    */
230   @Nullable
231   @Override
232   public Integer getDuration() {
233     // Returns duration value for tests
234     // or cached duration for suites
235     if (myDurationIsCached || !isSuite()) {
236       return myDuration;
237     }
238
239     //For suites counts and caches durations of its children. Also it evaluates partial duration,
240     //i.e. if duration is unknown it will be ignored in summary value.
241     //If duration for all children is unknown summary duration will be also unknown
242     //if one of children is ignored - it's duration will be 0 and if child wasn't run,
243     //then it's duration will be unknown
244     myDuration = calcSuiteDuration();
245     myDurationIsCached = true;
246
247     return myDuration;
248   }
249
250   @Override
251   public boolean shouldSkipRootNodeForExport() {
252     return true;
253   }
254
255   /**
256    * Sets duration of test
257    * @param duration In milliseconds
258    */
259   public void setDuration(final int duration) {
260     invalidateCachedDurationForContainerSuites();
261
262     if (!isSuite()) {
263       myDurationIsCached = true;
264       myDuration = (duration >= 0) ? duration : null;
265       return;
266     }
267
268     // Not allow to directly set duration for suites.
269     // It should be the sum of children. This requirement is only
270     // for safety of current model and may be changed
271     LOG.warn("Unsupported operation");
272   }
273
274   public void setFinished() {
275     if (myState.isFinal()) {
276       // we shouldn't fire new printable because final state
277       // has been already fired
278       return;
279     }
280
281     if (!isSuite()) {
282       // if isn't in other finished state (ignored, failed or passed)
283       myState = TestPassedState.INSTANCE;
284     } else {
285       //Test Suite
286       myState = determineSuiteStateOnFinished();
287     }
288     // prints final state additional info
289     fireOnNewPrintable(myState);
290   }
291
292   public void setTestFailed(@NotNull final String localizedMessage,
293                             @Nullable final String stackTrace,
294                             final boolean testError) {
295     myState = testError
296               ? new TestErrorState(localizedMessage, stackTrace)
297               : new TestFailedState(localizedMessage, stackTrace);
298     fireOnNewPrintable(myState);
299   }
300
301   public void setTestIgnored(@NotNull final String ignoreComment,
302                              @Nullable final String stackTrace) {
303     myState = new TestIgnoredState(ignoreComment, stackTrace);
304     fireOnNewPrintable(myState);
305   }
306
307   public void setParent(@Nullable final SMTestProxy parent) {
308     myParent = parent;
309   }
310
311   public List<? extends SMTestProxy> getChildren(@Nullable final Filter filter) {
312     final List<? extends SMTestProxy> allChildren = getChildren();
313
314     if (filter == Filter.NO_FILTER || filter == null) {
315       return allChildren;
316     }
317
318     final List<SMTestProxy> selectedChildren = new ArrayList<SMTestProxy>();
319     for (SMTestProxy child : allChildren) {
320       if (filter.shouldAccept(child)) {
321         selectedChildren.add(child);
322       }
323     }
324
325     if ((selectedChildren.isEmpty())) {
326       return Collections.<SMTestProxy>emptyList();
327     }
328     return selectedChildren;
329   }
330
331   public boolean wasLaunched() {
332     return myState.wasLaunched();
333   }
334
335
336   /**
337    * Prints this proxy and all its children on given printer
338    * @param printer Printer
339    */
340   public void printOn(final Printer printer) {
341     super.printOn(printer);
342
343     //Tests State, that provide and formats additional output
344     myState.printOn(printer);
345   }
346
347   public void addStdOutput(final String output, final Key outputType) {
348     addLast(new Printable() {
349       public void printOn(final Printer printer) {
350         printer.print(output, ConsoleViewContentType.getConsoleViewType(outputType));
351       }
352     });
353   }
354
355   public void addStdErr(final String output) {
356     addLast(new Printable() {
357       public void printOn(final Printer printer) {
358         printer.print(output, ConsoleViewContentType.ERROR_OUTPUT);
359       }
360     });
361   }
362
363   public void addError(final String output,
364                        @Nullable final String stackTrace) {
365     myHasErrors = true;
366     addLast(new Printable() {
367       public void printOn(final Printer printer) {
368         final String errorText = TestFailedState.buildErrorPresentationText(output, stackTrace);
369         LOG.assertTrue(errorText != null);
370
371         TestFailedState.printError(printer, errorText);
372       }
373     });
374   }
375
376   public void addSystemOutput(final String output) {
377     addLast(new Printable() {
378       public void printOn(final Printer printer) {
379         printer.print(output, ConsoleViewContentType.SYSTEM_OUTPUT);
380       }
381     });
382   }
383
384   @NotNull
385   public String getPresentableName() {
386     return TestsPresentationUtil.getPresentableName(this);
387   }
388
389   @Override
390   public String toString() {
391     return getPresentableName();
392   }
393
394   /**
395    * Process was terminated
396    */
397   public void setTerminated() {
398     if (myState.isFinal()) {
399       return;
400     }
401     myState = TerminatedState.INSTANCE;
402     final List<? extends SMTestProxy> children = getChildren();
403     for (SMTestProxy child : children) {
404       child.setTerminated();
405     }
406     fireOnNewPrintable(myState);
407   }
408
409   public boolean wasTerminated() {
410     return myState.wasTerminated();
411   }
412
413   @Nullable
414   protected String getLocationUrl() {
415     return myLocationUrl;
416   }
417
418   /**
419    * Check if suite contains error tests or suites
420    * @return True if contains
421    */
422   private boolean containsErrorTests() {
423     final List<? extends SMTestProxy> children = getChildren();
424     for (SMTestProxy child : children) {
425       if (child.getMagnitudeInfo() == TestStateInfo.Magnitude.ERROR_INDEX) {
426         return true;
427       }
428     }
429     return false;
430   }
431
432   private boolean containsFailedTests() {
433     final List<? extends SMTestProxy> children = getChildren();
434     for (SMTestProxy child : children) {
435       if (child.getMagnitudeInfo() == TestStateInfo.Magnitude.FAILED_INDEX) {
436         return true;
437       }
438     }
439     return false;
440   }
441
442   /**
443    * Determines site state after it has been finished
444    * @return New state
445    */
446   private AbstractState determineSuiteStateOnFinished() {
447     final AbstractState state;
448     if (isLeaf() || isEmptySuite()) {
449       state = SuiteFinishedState.EMPTY_SUITE;
450     } else {
451       if (isDefect()) {
452         // Test suit contains errors if at least one of its tests contains error
453         if (containsErrorTests()) {
454           state = SuiteFinishedState.ERROR_SUITE;
455         } else {
456           // if suite contains failed tests - all suite should be
457           // consider as failed
458           state = containsFailedTests()
459                    ? SuiteFinishedState.FAILED_SUITE
460                    : SuiteFinishedState.WITH_IGNORED_TESTS_SUITE;
461         }
462       } else {
463         state = SuiteFinishedState.PASSED_SUITE;
464       }
465     }
466     return state;
467   }
468
469   public boolean isEmptySuite() {
470     if (myIsEmptyIsCached) {
471       return myIsEmpty;
472     }
473
474     if (!isSuite()) {
475       // test - no matter what we will return
476       myIsEmpty = true;
477       myIsEmptyIsCached = true;
478
479       return myIsEmpty;
480     }
481
482     myIsEmpty = true;
483     final List<? extends SMTestProxy> allTestCases = getChildren();
484     for (SMTestProxy testOrSuite : allTestCases) {
485       if (testOrSuite.isSuite()) {
486         // suite
487         if (!testOrSuite.isEmptySuite()) {
488           // => parent suite isn't empty
489           myIsEmpty = false;
490           myIsEmptyIsCached = true;
491           break;
492         }
493         // all suites are empty
494         myIsEmpty = true;
495         // we can cache only final state, otherwise test may be added
496         myIsEmptyIsCached = myState.isFinal();
497       } else {
498         // test => parent suite isn't empty
499         myIsEmpty = false;
500         myIsEmptyIsCached = true;
501         break;
502       }
503     }
504     return myIsEmpty;
505   }
506
507
508
509   @Nullable
510   private Integer calcSuiteDuration() {
511     int partialDuration = 0;
512     boolean durationOfChildrenIsUnknown = true;
513
514     for (SMTestProxy child : getChildren()) {
515       final Integer duration = child.getDuration();
516       if (duration != null) {
517         durationOfChildrenIsUnknown = false;
518         partialDuration += duration.intValue();
519       }
520     }
521     // Lets convert partial duration in integer object. Negative partial duration
522     // means that duration of all children is unknown
523     return durationOfChildrenIsUnknown ? null : partialDuration;
524   }
525
526   /**
527    * Recursively invalidates cached duration for container(parent) suites
528    */
529   private void invalidateCachedDurationForContainerSuites() {
530     // Invalidates duration of this suite
531     myDuration = null;
532     myDurationIsCached = false;
533
534     // Invalidates duration of container suite
535     final SMTestProxy containerSuite = getParent();
536     if (containerSuite != null) {
537       containerSuite.invalidateCachedDurationForContainerSuites();
538     }
539   }
540 }