1package org.scalatest.tools 2 3import org.scalatools.testing._ 4import org.scalatest.tools.Runner.parsePropertiesArgsIntoMap 5import org.scalatest.tools.Runner.parseCompoundArgIntoSet 6import StringReporter.colorizeLinesIndividually 7import org.scalatest.Suite.formatterForSuiteStarting 8import org.scalatest.Suite.formatterForSuiteCompleted 9import org.scalatest.Suite.formatterForSuiteAborted 10import org.scalatest.events.SuiteStarting 11import org.scalatest.events.SuiteCompleted 12import org.scalatest.events.SuiteAborted 13 14/** 15 * Class that makes ScalaTest tests visible to sbt. 16 * 17 * <p> 18 * To use ScalaTest from within sbt, simply add a line like this to your project file, replacing 1.5 with whatever version you desire: 19 * </p> 20 * 21 * <pre class="stExamples"> 22 * val scalatest = "org.scalatest" % "scalatest_2.8.1" % "1.5" 23 * </pre> 24 * 25 * <p> 26 * You can configure the output shown when running with sbt in four ways: 1) turn off color, 2) show 27 * short stack traces, 3) full stack traces, and 4) show durations for everything. To do that 28 * you need to add test options, like this: 29 * </p> 30 * 31 * <pre class="stExamples"> 32 * override def testOptions = super.testOptions ++ 33 * Seq(TestArgument(TestFrameworks.ScalaTest, "-oD")) 34 * </pre> 35 * 36 * <p> 37 * After the -o, place any combination of: 38 * </p> 39 * 40 * <ul> 41 * <li>D - show durations</li> 42 * <li>S - show short stack traces</li> 43 * <li>F - show full stack traces</li> 44 * <li>W - without color</li> 45 * </ul> 46 * 47 * <p> 48 * For example, "-oDF" would show full stack traces and durations (the amount 49 * of time spent in each test). 50 * </p> 51 * 52 * @author Bill Venners 53 * @author Josh Cough 54 */ 55class ScalaTestFramework extends Framework { 56 57 /** 58 * Returns <code>"ScalaTest"</code>, the human readable name for this test framework. 59 */ 60 def name = "ScalaTest" 61 62 /** 63 * Returns an array containing one <code>org.scalatools.testing.TestFingerprint</code> object, whose superclass name is <code>org.scalatest.Suite</code> 64 * and <code>isModule</code> value is false. 65 */ 66 def tests = 67 Array( 68 new org.scalatools.testing.TestFingerprint { 69 def superClassName = "org.scalatest.Suite" 70 def isModule = false 71 } 72 ) 73 74 /** 75 * Returns an <code>org.scalatools.testing.Runner</code> that will load test classes via the passed <code>testLoader</code> 76 * and direct output from running the tests to the passed array of <code>Logger</code>s. 77 */ 78 def testRunner(testLoader: ClassLoader, loggers: Array[Logger]) = { 79 new ScalaTestRunner(testLoader, loggers) 80 } 81 82 /**The test runner for ScalaTest suites. It is compiled in a second step after the rest of sbt.*/ 83 private[tools] class ScalaTestRunner(val testLoader: ClassLoader, val loggers: Array[Logger]) extends org.scalatools.testing.Runner { 84 85 import org.scalatest._ 86 87 /* 88 test-only FredSuite -- -A -B -C -d all things to right of == come in as a separate string in the array 89 the other way is to set up the options and when I say test it always comes in that way 90 91 new wqay, if one framework 92 93testOptions in Test += Tests.Arguments("-d", "-g") 94 95so each of those would come in as one separate string in the aray 96 97testOptions in Test += Tests.Arguments(TestFrameworks.ScalaTest, "-d", "-g") 98 99Remember: 100 101maybe add a distributor like thing to run 102maybe add some event things like pending, ignored as well skipped 103maybe a call back for the summary 104 105st look at wiki on xsbt 106 107tasks & commands. commands have full control over everything. 108tasks are more integrated, don't need to know as much. 109write a sbt plugin to deploy the task. 110 111 */ 112 def run(testClassName: String, fingerprint: TestFingerprint, eventHandler: EventHandler, args: Array[String]) { 113 val suiteClass = Class.forName(testClassName, true, testLoader).asSubclass(classOf[Suite]) 114 115 // println("sbt args: " + args.toList) 116 if (isAccessibleSuite(suiteClass)) { 117 118 val (propertiesArgsList, includesArgsList, 119 excludesArgsList, repoArg) = parsePropsAndTags(args.filter(!_.equals(""))) 120 val configMap: Map[String, String] = parsePropertiesArgsIntoMap(propertiesArgsList) 121 val tagsToInclude: Set[String] = parseCompoundArgIntoSet(includesArgsList, "-n") 122 val tagsToExclude: Set[String] = parseCompoundArgIntoSet(excludesArgsList, "-l") 123 val filter = org.scalatest.Filter(if (tagsToInclude.isEmpty) None else Some(tagsToInclude), tagsToExclude) 124 125 val (presentAllDurations, presentInColor, presentShortStackTraces, presentFullStackTraces) = 126 repoArg match { 127 case Some(arg) => ( 128 arg contains 'D', 129 !(arg contains 'W'), 130 arg contains 'S', 131 arg contains 'F' 132 ) 133 case None => (false, true, false, false) 134 } 135 136 val report = new ScalaTestReporter(eventHandler, presentAllDurations, presentInColor, presentShortStackTraces, presentFullStackTraces) 137 138 val tracker = new Tracker 139 val suiteStartTime = System.currentTimeMillis 140 141 val suite = suiteClass.newInstance 142 143 val formatter = formatterForSuiteStarting(suite) 144 145 report(SuiteStarting(tracker.nextOrdinal(), suite.suiteName, Some(suiteClass.getName), formatter, None)) 146 147 try { 148 suite.run(None, report, new Stopper {}, filter, configMap, None, tracker) 149 150 val formatter = formatterForSuiteCompleted(suite) 151 152 val duration = System.currentTimeMillis - suiteStartTime 153 report(SuiteCompleted(tracker.nextOrdinal(), suite.suiteName, Some(suiteClass.getName), Some(duration), formatter, None)) 154 } 155 catch { 156 case e: Exception => { 157 158 // TODO: Could not get this from Resources. Got: 159 // java.util.MissingResourceException: Can't find bundle for base name org.scalatest.ScalaTestBundle, locale en_US 160 val rawString = "Exception encountered when attempting to run a suite with class name: " + suiteClass.getName 161 val formatter = formatterForSuiteAborted(suite, rawString) 162 163 val duration = System.currentTimeMillis - suiteStartTime 164 report(SuiteAborted(tracker.nextOrdinal(), rawString, suite.suiteName, Some(suiteClass.getName), Some(e), Some(duration), formatter, None)) 165 } 166 } 167 } 168 else throw new IllegalArgumentException("Class is not an accessible org.scalatest.Suite: " + testClassName) 169 } 170 171 private val emptyClassArray = new Array[java.lang.Class[T] forSome {type T}](0) 172 173 private def isAccessibleSuite(clazz: java.lang.Class[_]): Boolean = { 174 import java.lang.reflect.Modifier 175 176 try { 177 classOf[Suite].isAssignableFrom(clazz) && 178 Modifier.isPublic(clazz.getModifiers) && 179 !Modifier.isAbstract(clazz.getModifiers) && 180 Modifier.isPublic(clazz.getConstructor(emptyClassArray: _*).getModifiers) 181 } catch { 182 case nsme: NoSuchMethodException => false 183 case se: SecurityException => false 184 } 185 } 186 187 private class ScalaTestReporter(eventHandler: EventHandler, presentAllDurations: Boolean, 188 presentInColor: Boolean, presentShortStackTraces: Boolean, presentFullStackTraces: Boolean) extends StringReporter( 189 presentAllDurations, presentInColor, presentShortStackTraces, presentFullStackTraces) { 190 191 import org.scalatest.events._ 192 193 protected def printPossiblyInColor(text: String, ansiColor: String) { 194 import PrintReporter.ansiReset 195 loggers.foreach { logger => 196 logger.info(if (logger.ansiCodesSupported && presentInColor) colorizeLinesIndividually(text, ansiColor) else text) 197 } 198 } 199 200 def dispose() = () 201 202 def fireEvent(tn: String, r: Result, e: Option[Throwable]) = { 203 eventHandler.handle( 204 new org.scalatools.testing.Event { 205 def testName = tn 206 def description = tn 207 def result = r 208 def error = e getOrElse null 209 } 210 ) 211 } 212 213 override def apply(event: Event) { 214 215 // Superclass will call printPossiblyInColor 216 super.apply(event) 217 218 // Logging done, all I need to do now is fire events 219 event match { 220 // the results of running an actual test 221 case t: TestPending => fireEvent(t.testName, Result.Skipped, None) 222 case t: TestFailed => fireEvent(t.testName, Result.Failure, t.throwable) 223 case t: TestSucceeded => fireEvent(t.testName, Result.Success, None) 224 case t: TestIgnored => fireEvent(t.testName, Result.Skipped, None) 225 case _ => 226 } 227 } 228 } 229 230 private[scalatest] def parsePropsAndTags(args: Array[String]) = { 231 232 import collection.mutable.ListBuffer 233 234 val props = new ListBuffer[String]() 235 val includes = new ListBuffer[String]() 236 val excludes = new ListBuffer[String]() 237 var repoArg: Option[String] = None 238 239 val it = args.iterator 240 while (it.hasNext) { 241 242 val s = it.next 243 244 if (s.startsWith("-D")) { 245 props += s 246 } 247 else if (s.startsWith("-n")) { 248 includes += s 249 if (it.hasNext) 250 includes += it.next 251 } 252 else if (s.startsWith("-l")) { 253 excludes += s 254 if (it.hasNext) 255 excludes += it.next 256 } 257 else if (s.startsWith("-o")) { 258 if (repoArg.isEmpty) // Just use first one. Ignore any others. 259 repoArg = Some(s) 260 } 261 // else if (s.startsWith("-t")) { 262 // 263 // testNGXMLFiles += s 264 // if (it.hasNext) 265 // testNGXMLFiles += it.next 266 // } 267 else { 268 throw new IllegalArgumentException("Unrecognized argument: " + s) 269 } 270 } 271 (props.toList, includes.toList, excludes.toList, repoArg) 272 } 273 } 274} 275