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