1/*
2 * Copyright 2001-2008 Artima, Inc.
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 */
16package org.scalatest
17
18import java.util.concurrent.CountDownLatch
19import scala.actors.Exit
20import scala.actors.Actor
21import scala.actors.Actor.actor
22import scala.actors.Actor.loop
23import scala.actors.Actor.receive
24import java.io.PrintStream
25import org.scalatest.events._
26import DispatchReporter.propagateDispose
27
28/**
29 * A <code>Reporter</code> that dispatches test results to other <code>Reporter</code>s.
30 * Attempts to dispatch each method invocation to each contained <code>Reporter</code>,
31 * even if some <code>Reporter</code> methods throw <code>Exception</code>s. Catches
32 * <code>Exception</code>s thrown by <code>Reporter</code> methods and prints error
33 * messages to the standard error stream.
34 *
35 * The primary constructor creates a new <code>DispatchReporter</code> with specified <code>Reporter</code>s list.
36 * Each object in the <code>reporters</code> list must implement <code>Reporter</code>.
37 *
38 * @param reporters the initial <code>Reporter</code>s list for this
39 * <code>DispatchReporter</code>
40 * @throws NullPointerException if <code>reporters</code> is <code>null</code>.
41 * @author Bill Venners
42 */
43private[scalatest] class DispatchReporter(val reporters: List[Reporter], out: PrintStream) extends Reporter {
44
45  private case object Dispose
46
47
48  private val latch = new CountDownLatch(1)
49
50  private val julia = actor {
51
52    var alive = true // local variable. Only used by the Actor's thread, so no need for synchronization
53
54    class Counter {
55      var testsSucceededCount = 0
56      var testsFailedCount = 0
57      var testsIgnoredCount = 0
58      var testsPendingCount = 0
59      var suitesCompletedCount = 0
60      var suitesAbortedCount = 0
61    }
62
63    val counterMap = scala.collection.mutable.Map[Int, Counter]()
64
65    def incrementCount(event: Event, f: (Counter) => Unit) {
66      val runStamp = event.ordinal.runStamp
67      if (counterMap.contains(runStamp)) {
68        val counter = counterMap(runStamp)
69        f(counter)
70      }
71      else {
72        val counter = new Counter
73        f(counter)
74        counterMap(runStamp) = counter
75      }
76    }
77
78    // If None, that means don't update the summary so forward the old event. If Some,
79    // create a new event with everything the same except the old summary replaced by the new one
80    def updatedSummary(oldSummary: Option[Summary], ordinal: Ordinal): Option[Summary] = {
81      oldSummary match {
82        case None if (counterMap.contains(ordinal.runStamp)) => {
83            // Update the RunAborted so that it is the same except it has a new Some(Summary)
84            val counter = counterMap(ordinal.runStamp)
85            Some(
86              Summary(
87                counter.testsSucceededCount,
88                counter.testsFailedCount,
89                counter.testsIgnoredCount,
90                counter.testsPendingCount,
91                counter.suitesCompletedCount,
92                counter.suitesAbortedCount
93              )
94            )
95          }
96         case _ => None // Also pass the old None summary through if it isn't in the counterMap
97      }
98    }
99
100    while (alive) {
101      receive {
102        case event: Event =>
103          try {
104            // The event will only actually be updated if it it is a RunCompleted/Aborted/Stopped event with None
105            // as its summary and its runstamp has a counter entry. In that case, it will be given a Summary taken
106            // from the counter. (And the counter will be removed from the counterMap.) These are counted here, because
107            // they need to be counted on this side of any FilterReporters that may be in place. (In early versions of
108            // ScalaTest, these were wrongly being counted by the reporters themselves, so if a FilterReporter filtered
109            // out TestSucceeded events, then they just weren't being counted.
110            val updatedEvent =
111              event match {
112
113                case _: RunStarting => counterMap(event.ordinal.runStamp) = new Counter; event
114
115                case _: TestSucceeded => incrementCount(event, _.testsSucceededCount += 1); event
116                case _: TestFailed => incrementCount(event, _.testsFailedCount += 1); event
117                case _: TestIgnored => incrementCount(event, _.testsIgnoredCount += 1); event
118                case _: TestPending => incrementCount(event, _.testsPendingCount += 1); event
119                case _: SuiteCompleted => incrementCount(event, _.suitesCompletedCount += 1); event
120                case _: SuiteAborted => incrementCount(event, _.suitesAbortedCount += 1); event
121
122                case oldRunCompleted @ RunCompleted(ordinal, duration, summary, formatter, payload, threadName, timeStamp) =>
123                  updatedSummary(summary, ordinal) match {
124                    case None => oldRunCompleted
125                    case newSummary @ Some(_) =>
126                      counterMap.remove(ordinal.runStamp)
127                      // Update the RunCompleted so that it is the same except it has a new Some(Summary)
128                      RunCompleted(ordinal, duration, newSummary, formatter, payload, threadName, timeStamp)
129                  }
130
131                case oldRunStopped @ RunStopped(ordinal, duration, summary, formatter, payload, threadName, timeStamp) =>
132                  updatedSummary(summary, ordinal) match {
133                    case None => oldRunStopped
134                    case newSummary @ Some(_) =>
135                      counterMap.remove(ordinal.runStamp)
136                      // Update the RunStopped so that it is the same except it has a new Some(Summary)
137                      RunStopped(ordinal, duration, newSummary, formatter, payload, threadName, timeStamp)
138                  }
139
140                case oldRunAborted @ RunAborted(ordinal, message, throwable, duration, summary, formatter, payload, threadName, timeStamp) =>
141                  updatedSummary(summary, ordinal) match {
142                    case None => oldRunAborted
143                    case newSummary @ Some(_) =>
144                      counterMap.remove(ordinal.runStamp)
145                      // Update the RunAborted so that it is the same except it has a new Some(Summary)
146                      RunAborted(ordinal, message, throwable, duration, newSummary, formatter, payload, threadName, timeStamp)
147                  }
148
149                case _ => event
150              }
151            for (report <- reporters)
152              report(updatedEvent)
153          }
154          catch {
155            case e: Exception =>
156              val stringToPrint = Resources("reporterThrew", event)
157              out.println(stringToPrint)
158              e.printStackTrace(out)
159          }
160        case Dispose =>
161          try {
162            for (reporter <- reporters)
163              propagateDispose(reporter)
164          }
165          catch {
166            case e: Exception =>
167              val stringToPrint = Resources("reporterDisposeThrew")
168              out.println(stringToPrint)
169              e.printStackTrace(out)
170          }
171          finally {
172            alive = false
173            latch.countDown()
174          }
175      }
176    }
177  }
178
179  def this(reporters: List[Reporter]) = this(reporters, System.out)
180  def this(reporter: Reporter) = this(List(reporter), System.out)
181
182  // Invokes dispose on each Reporter in this DispatchReporter's reporters list.
183  // This method fires an event at the actor that is taking care of serializing
184  // events, and at some time later the actor's thread will attempts to invoke
185  // dispose on each contained Reporter, even if some Reporter's dispose methods throw
186  // Exceptions. This method catches any Exception thrown by
187  // a dispose method and handles it by printing an error message to the
188  // standard error stream. Once finished with that, the actor's thread will return.
189  //
190  // This method will not return until the actor's thread has exited.
191  //
192  def dispatchDisposeAndWaitUntilDone() {
193    julia ! Dispose
194    latch.await()
195  }
196
197  def apply(event: Event) {
198    julia ! event
199  }
200}
201
202// TODO: Not a real problem, but if a DispatchReporter ever got itself in
203// its list of reporters, this would end up being an infinite loop. But
204// That first part, a DispatchReporter getting itself in there would be the real
205// bug.
206private[scalatest] object DispatchReporter {
207
208  def propagateDispose(reporter: Reporter) {
209    reporter match {
210      case dispatchReporter: DispatchReporter => dispatchReporter.dispatchDisposeAndWaitUntilDone()
211      case catchReporter: CatchReporter => catchReporter.catchDispose()
212      case resourcefulReporter: ResourcefulReporter => resourcefulReporter.dispose()
213      case _ =>
214    }
215  }
216}
217