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