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.tools
17
18import org.scalatest._
19import scala.collection.mutable.ListBuffer
20import java.lang.reflect.Constructor
21import java.lang.reflect.Modifier
22import java.net.URL
23import java.net.MalformedURLException
24import java.net.URLClassLoader
25import java.io.File
26import java.io.IOException
27import javax.swing.SwingUtilities
28import java.util.concurrent.ArrayBlockingQueue
29import java.util.regex.Pattern
30import org.scalatest.testng.TestNGWrapperSuite
31import java.util.concurrent.Semaphore
32import org.scalatest.events._
33import org.scalatest.junit.JUnitWrapperSuite
34import java.util.concurrent.Executors
35import java.util.concurrent.ExecutorService
36
37/**
38 * <p>
39 * Application that runs a suite of tests.
40 * The application accepts command line arguments that specify optional <em>config map</em> (key-value pairs), an optional
41 * <em>runpath</em>, zero to many <code>Reporter</code>s, optional lists of tags to include and/or exclude, zero to many
42 * <code>Suite</code> class names, zero to many "members-only" <code>Suite</code> paths, zero to many "wildcard" <code>Suite</code> paths,
43 * and zero to many TestNG XML config file paths.
44 * All of these arguments are described in more detail below. Here's a summary:
45 * </p>
46 *
47 * <pre class="stExamples">
48 * scala [-classpath scalatest-&lt;version&gt;.jar:...] org.scalatest.tools.Runner
49[-D&lt;key&gt;=&lt;value&gt; [...]] [-p &lt;runpath&gt;] [reporter [...]]
50[-n &lt;includes&gt;] [-l &lt;excludes&gt;] [-c] [-s &lt;suite class name&gt;
51[...]] [-j &lt;junit class name&gt; [...]] [-m &lt;members-only suite path&gt;
52[...]] [-w &lt;wildcard suite path&gt; [...]] [-t &lt;TestNG config file
53path&gt; [...]]
54 * </pre>
55 *
56 * <p>
57 * The simplest way to start <code>Runner</code> is to specify the directory containing your compiled tests as the sole element of the runpath, for example:
58 * </p>
59 *
60 * <pre class="stExamples">scala -classpath scalatest-&lt;version&gt;.jar org.scalatest.tools.Runner -p compiled_tests</pre>
61 *
62 * <p>
63 * Given the previous command, <code>Runner</code> will discover and execute all <code>Suite</code>s in the <code>compiled_tests</code> directory and its subdirectories,
64 * and show results in graphical user interface (GUI).
65 * </p>
66 *
67 * <a name="configMapSection"></a>
68 * <h2>Specifying the config map</h2>
69 *
70 * <p>
71 * A <em>config map</em> contains pairs consisting of a string key and a value that may be of any type. (Keys that start with
72 * &quot;org.scalatest.&quot; are reserved for ScalaTest. Configuration values that are themselves strings may be specified on the
73 * <code>Runner</code> command line.
74 * Each configuration pair is denoted with a "-D", followed immediately by the key string, an &quot;=&quot;, and the value string.
75 * For example:
76 * </p>
77 *
78 * <pre class="stExamples">-Ddbname=testdb -Dserver=192.168.1.188</pre>
79 *
80 * <h2>Specifying a runpath</h2>
81 *
82 * <p>
83 * A runpath is the list of filenames, directory paths, and/or URLs that <code>Runner</code>
84 * uses to load classes for the running test. If runpath is specified, <code>Runner</code> creates
85 * a custom class loader to load classes available on the runpath.
86 * The graphical user interface reloads the test classes anew for each run
87 * by creating and using a new instance of the custom class loader for each run.
88 * The classes that comprise the test may also be made available on
89 * the classpath, in which case no runpath need be specified.
90 * </p>
91 *
92 * <p>
93 * The runpath is specified with the <b>-p</b> option. The <b>-p</b> must be followed by a space,
94 * a double quote (<code>"</code>), a white-space-separated list of
95 * paths and URLs, and a double quote. If specifying only one element in the runpath, you can leave off
96 * the double quotes, which only serve to combine a white-space separated list of strings into one
97 * command line argument. If you have path elements that themselves have a space in them, you must
98 * place a backslash (\) in front of the space. Here's an example:
99 * </p>
100 *
101 * <pre class="stExamples">-p "serviceuitest-1.1beta4.jar myjini http://myhost:9998/myfile.jar target/class\ files"</pre>
102 *
103 * <h2>Specifying reporters</h2>
104 *
105 * <p>
106 * Reporters can be specified  on the command line in any of the following
107 * ways:
108 * </p>
109 *
110 * <ul>
111 * <li> <code><b>-g[configs...]</b></code> - causes display of a graphical user interface that allows
112 *    tests to be run and results to be investigated</li>
113 * <li> <code><b>-f[configs...] &lt;filename&gt;</b></code> - causes test results to be written to
114 *     the named file</li>
115 * <li> <code><b>-u &lt;directory&gt;</b></code> - causes test results to be written to
116 *      xml files in the named directory</li>
117 * <li> <code><b>-o[configs...]</b></code> - causes test results to be written to
118 *     the standard output</li>
119 * <li> <code><b>-e[configs...]</b></code> - causes test results to be written to
120 *     the standard error</li>
121 * <li> <code><b>-r[configs...] &lt;reporterclass&gt;</b></code> - causes test results to be reported to
122 *     an instance of the specified fully qualified <code>Reporter</code> class name</li>
123 * </ul>
124 *
125 * <p>
126 * The <code><b>[configs...]</b></code> parameter, which is used to configure reporters, is described in the next section.
127 * </p>
128 *
129 * <p>
130 * The <code><b>-r</b></code> option causes the reporter specified in
131 * <code><b>&lt;reporterclass&gt;</b></code> to be
132 * instantiated.
133 * Each reporter class specified with a <b>-r</b> option must be public, implement
134 * <code>org.scalatest.Reporter</code>, and have a public no-arg constructor.
135 * Reporter classes must be specified with fully qualified names.
136 * The specified reporter classes may be
137 * deployed on the classpath. If a runpath is specified with the
138 * <code>-p</code> option, the specified reporter classes may also be loaded from the runpath.
139 * All specified reporter classes will be loaded and instantiated via their no-arg constructor.
140 * </p>
141 *
142 * <p>
143 * For example, to run a suite named <code>MySuite</code> from the <code>mydir</code> directory
144 * using two reporters, the graphical reporter and a file reporter
145 * writing to a file named <code>"test.out"</code>, you would type:
146 * </p>
147 *
148 * <pre class="stExamples">java -jar scalatest.jar -p mydir <b>-g -f test.out</b> -s MySuite</pre>
149 *
150 * <p>
151 * The <code><b>-g</b></code>, <code><b>-o</b></code>, or <code><b>-e</b></code> options can
152 * appear at most once each in any single command line.
153 * Multiple appearances of <code><b>-f</b></code> and <code><b>-r</b></code> result in multiple reporters
154 * unless the specified <code><b>&lt;filename&gt;</b></code> or <code><b>&lt;reporterclass&gt;</b></code> is
155 * repeated. If any of <code><b>-g</b></code>, <code><b>-o</b></code>, <code><b>-e</b></code>,
156 * <code><b>&lt;filename&gt;</b></code> or <code><b>&lt;reporterclass&gt;</b></code> are repeated on
157 * the command line, the <code>Runner</code> will print an error message and not run the tests.
158 * </p>
159 *
160 * <p>
161 * <code>Runner</code> adds the reporters specified on the command line to a <em>dispatch reporter</em>,
162 * which will dispatch each method invocation to each contained reporter. <code>Runner</code> will pass
163 * the dispatch reporter to executed suites. As a result, every
164 * specified reporter will receive every report generated by the running suite of tests.
165 * If no reporters are specified, a graphical
166 * runner will be displayed that provides a graphical report of
167 * executed suites.
168 * </p>
169 *
170 * <h2>Configuring Reporters</h2>
171 *
172 * <p>
173 * Each reporter option on the command line can include configuration characters. Configuration characters
174 * are specified immediately following the <code><b>-g</b></code>, <code><b>-o</b></code>,
175 * <code><b>-e</b></code>, <code><b>-f</b></code>, or <code><b>-r</b></code>. The following configuration
176 * characters, which cause reports to be dropped, are valid for any reporter:
177 * </p>
178 *
179 * <ul>
180 * <li> <code><b>N</b></code> - drop <code>TestStarting</code> events</li>
181 * <li> <code><b>C</b></code> - drop <code>TestSucceeded</code> events</li>
182 * <li> <code><b>X</b></code> - drop <code>TestIgnored</code> events</li>
183 * <li> <code><b>E</b></code> - drop <code>TestPending</code> events</li>
184 * <li> <code><b>H</b></code> - drop <code>SuiteStarting</code> events</li>
185 * <li> <code><b>L</b></code> - drop <code>SuiteCompleted</code> events</li>
186 * <li> <code><b>O</b></code> - drop <code>InfoProvided</code> events</li>
187 * </ul>
188 *
189 * <p>
190 * A dropped event will not be delivered to the reporter at all. So the reporter will not know about it and therefore not
191 * present information about the event in its report. For example, if you specify <code>-oN</code>, the standard output reporter
192 * will never receive any <code>TestStarting</code> events and will therefore never report them. The purpose of these
193 * configuration parameters is to allow users to selectively remove events they find add clutter to the report without
194 * providing essential information.
195 * </p>
196 *
197 * <p>
198 * The following three reporter configuration parameters may additionally be used on standard output (-o), standard error (-e),
199 * and file (-f) reporters:
200 * </p>
201 *
202 * <ul>
203 * <li> <code><b>W</b></code> - without color</li>
204 * <li> <code><b>D</b></code> - show all durations</li>
205 * <li> <code><b>S</b></code> - show short stack traces</li>
206 * <li> <code><b>F</b></code> - show full stack traces</li>
207 * </ul>
208 *
209 * <p>
210 * If you specify a W, D, S, or F for any reporter other than standard output, standard error, or file reporters, <code>Runner</code>
211 * will complain with an error message and not perform the run.
212 * </p>
213 *
214 * <p>
215 * Configuring a standard output, error, or file reporter with <code>D</code> will cause that reporter to
216 * print a duration for each test and suite.  When running in the default mode, a duration will only be printed for
217 * the entire run.
218 * </p>
219 *
220 * <p>
221 * Configuring a standard output, error, or file reporter with <code>F</code> will cause that reporter to print full stack traces for all exceptions,
222 * including <code>TestFailedExceptions</code>. Every <code>TestFailedException</code> contains a stack depth of the
223 * line of test code that failed so that users won't need to search through a stack trace to find it. When running in the default,
224 * mode, these reporters will only show full stack traces when other exceptions are thrown, such as an exception thrown
225 * by production code. When a <code>TestFailedException</code> is thrown in default mode, only the source filename and
226 * line number of the line of test code that caused the test to fail are printed along with the error message, not the full stack
227 * trace.
228 * </p>
229 *
230 * <p>
231 * By default, a standard output, error, or file reporter inserts ansi escape codes into the output printed to change and later reset
232 * terminal colors. Information printed as a result of run starting, completed, and stopped events
233 * is printed in cyan. Information printed as a result of ignored or pending test events is shown in yellow. Information printed
234 * as a result of test failed, suite aborted, or run aborted events is printed in red. All other information is printed in green.
235 * The purpose of these colors is to facilitate speedy reading of the output, especially the finding of failed tests, which can
236 * get lost in a sea of passing tests. Configuring a standard output, error, or file reporter into without-color mode ('W') will
237 * turn off this behavior. No ansi codes will be inserted.
238 * </p>
239 *
240 * <p>
241 * For example, to run a suite using two reporters, the graphical reporter configured to present every reported event
242 * and a standard error reporter configured to present everything but test starting, test succeeded, test ignored, test
243 * pending, suite starting, suite completed, and info provided events, you would type:
244 * </p>
245 *
246 * <p>
247 * <code>scala -classpath scalatest-&lt;version&gt;.jar -p mydir <strong>-g -eNDXEHLO</strong> -s MySuite</code>
248 * </p>
249 *
250 * <p>
251 * Note that no white space is allowed between the reporter option and the initial configuration
252 * parameters. So <code>"-e NDXEHLO"</code> will not work,
253 * <code>"-eNDXEHLO"</code> will work.
254 * </p>
255 *
256 * <h2>Specifying tags to include and exclude</h2>
257 *
258 * <p>
259 * You can specify tag names of tests to include or exclude from a run. To specify tags to include,
260 * use <code>-n</code> followed by a white-space-separated list of tag names to include, surrounded by
261 * double quotes. (The double quotes are not needed if specifying just one tag.)  Similarly, to specify tags
262 * to exclude, use <code>-l</code> followed by a white-space-separated
263 * list of tag names to exclude, surrounded by double quotes. (As before, the double quotes are not needed
264 * if specifying just one tag.) If tags to include is not specified, then all tests
265 * except those mentioned in the tags to exclude (and in the <code>org.scalatest.Ignore</code> tag), will be executed.
266 * (In other words, the absence of a <code>-n</code> option is like a wildcard, indicating all tests be included.)
267 * If tags to include is specified, then only those tests whose tags are mentioned in the argument following <code>-n</code>
268 * and not mentioned in the tags to exclude, will be executed. For more information on test tags, see
269 * the <a href="../Suite.html">documentation for <code>Suite</code></a>. Here are some examples:
270 * </p>
271 *
272 * <p>
273 * <ul>
274 * <li><code>-n CheckinTests</code></li>
275 * <li><code>-n FunctionalTests -l SlowTests</code></li>
276 * <li><code>-n "CheckinTests FunctionalTests" -l "SlowTests NetworkTests"</code></li>
277 * </ul>
278 * </p>
279 *
280 * <h2>Executing <code>Suite</code>s in parallel</h2>
281 *
282 * <p>
283 * With the proliferation of multi-core architectures, and the often parallelizable nature of tests, it is useful to be able to run
284 * tests in parallel. If you include <code>-c</code> on the command line, <code>Runner</code> will pass a <code>Distributor</code> to
285 * the <code>Suite</code>s you specify with <code>-s</code>. <code>Runner</code> will set up a thread pool to execute any <code>Suite</code>s
286 * passed to the <code>Distributor</code>'s <code>put</code> method in parallel. Trait <code>Suite</code>'s implementation of
287 * <code>runNestedSuites</code> will place any nested <code>Suite</code>s into this <code>Distributor</code>. Thus, if you have a <code>Suite</code>
288 * of tests that must be executed sequentially, you should override <code>runNestedSuites</code> as described in the <a href="../Distributor.html">documentation for <code>Distributor</code></a>.
289 * </p>
290 *
291 * <p>
292 * The <code>-c</code> option may optionally be appended with a number (e.g.
293 * "<code>-c10</code>" -- no intervening space) to specify the number of
294 * threads to be created in the thread pool.  If no number (or 0) is
295 * specified, the number of threads will be decided based on the number of
296 * processors available.
297 * </p>
298 *
299 * <h2>Specifying <code>Suite</code>s</h2>
300 *
301 * <p>
302 * Suites are specified on the command line with a <b>-s</b> followed by the fully qualified
303 * name of a <code>Suite</code> subclass, as in:
304 * </p>
305 *
306 * <pre class="stExamples">-s com.artima.serviceuitest.ServiceUITestkit</pre>
307 *
308 * <p>
309 * Each specified suite class must be public, a subclass of
310 * <code>org.scalatest.Suite</code>, and contain a public no-arg constructor.
311 * <code>Suite</code> classes must be specified with fully qualified names.
312 * The specified <code>Suite</code> classes may be
313 * loaded from the classpath. If a runpath is specified with the
314 * <code>-p</code> option, specified <code>Suite</code> classes may also be loaded from the runpath.
315 * All specified <code>Suite</code> classes will be loaded and instantiated via their no-arg constructor.
316 * </p>
317 *
318 * <p>
319 * The runner will invoke <code>execute</code> on each instantiated <code>org.scalatest.Suite</code>,
320 * passing in the dispatch reporter to each <code>execute</code> method.
321 * </p>
322 *
323 * <p>
324 * <code>Runner</code> is intended to be used from the command line. It is included in <code>org.scalatest</code>
325 * package as a convenience for the user. If this package is incorporated into tools, such as IDEs, which take
326 * over the role of runner, object <code>org.scalatest.tools.Runner</code> may be excluded from that implementation of the package.
327 * All other public types declared in package <code>org.scalatest.tools.Runner</code> should be included in any such usage, however,
328 * so client software can count on them being available.
329 * </p>
330 *
331 * <a name="membersOnlyWildcard">
332 * <h2>Specifying "members-only" and "wildcard" <code>Suite</code> paths</h2>
333 * </a>
334 *
335 * <p>
336 * If you specify <code>Suite</code> path names with <code>-m</code> or <code>-w</code>, <code>Runner</code> will automatically
337 * discover and execute accessible <code>Suite</code>s in the runpath that are either a member of (in the case of <code>-m</code>)
338 * or enclosed by (in the case of <code>-w</code>) the specified path. As used in this context, a <em>path</em> is a portion of a fully qualified name.
339 * For example, the fully qualifed name <code>com.example.webapp.MySuite</code> contains paths <code>com</code>, <code>com.example</code>, and <code>com.example.webapp</code>.
340 * The fully qualifed name <code>com.example.webapp.MyObject.NestedSuite</code> contains paths <code>com</code>, <code>com.example</code>,
341 * <code>com.example.webapp</code>, and <code>com.example.webapp.MyObject</code>.
342 * An <em>accessible <code>Suite</code></em> is a public class that extends <code>org.scalatest.Suite</code>
343 * and defines a public no-arg constructor. Note that <code>Suite</code>s defined inside classes and traits do not have no-arg constructors,
344 * and therefore won't be discovered. <code>Suite</code>s defined inside singleton objects, however, do get a no-arg constructor by default, thus
345 * they can be discovered.
346 * </p>
347 *
348 * <p>
349 * For example, if you specify <code>-m com.example.webapp</code>
350 * on the command line, and you've placed <code>com.example.webapp.RedSuite</code> and <code>com.example.webapp.BlueSuite</code>
351 * on the runpath, then <code>Runner</code> will instantiate and execute both of those <code>Suite</code>s. The difference
352 * between <code>-m</code> and <code>-w</code> is that for <code>-m</code>, only <code>Suite</code>s that are direct members of the named path
353 * will be discovered. For <code>-w</code>, any <code>Suite</code>s whose fully qualified
354 * name begins with the specified path will be discovered. Thus, if <code>com.example.webapp.controllers.GreenSuite</code>
355 * exists on the runpath, invoking <code>Runner</code> with <code>-w com.example.webapp</code> will cause <code>GreenSuite</code>
356 * to be discovered, because its fully qualifed name begins with <code>"com.example.webapp"</code>. But if you invoke <code>Runner</code>
357 * with <code>-m com.example.webapp</code>, <code>GreenSuite</code> will <em>not</em> be discovered because it is directly
358 * a member of <code>com.example.webapp.controllers</code>, not <code>com.example.webapp</code>.
359 * </p>
360 *
361 * <p>
362 * If you specify no <code>-s</code>, <code>-m</code>, or <code>-w</code> arguments on the command line to <code>Runner</code>, it will discover and execute all accessible <code>Suite</code>s
363 * in the runpath.
364 * </p>
365 *
366 * <h2>Specifying TestNG XML config file paths</h2>
367 *
368 * <p>
369 * If you specify one or more file paths with <code>-t</code>, <code>Runner</code> will create a <code>org.scalatest.testng.TestNGWrapperSuite</code>,
370 * passing in a <code>List</code> of the specified paths. When executed, the <code>TestNGWrapperSuite</code> will create one <code>TestNG</code> instance
371 * and pass each specified file path to it for running. If you include <code>-t</code> arguments, you must include TestNG's jar file on the class path or runpath.
372 * The <code>-t</code> argument will enable you to run existing <code>TestNG</code> tests, including tests written in Java, as part of a ScalaTest run.
373 * You need not use <code>-t</code> to run suites written in Scala that extend <code>TestNGSuite</code>. You can simply run such suites with
374 * <code>-s</code>, <code>-m</code>, or </code>-w</code> parameters.
375 * </p>
376 *
377 * <h2>Specifying JUnit tests</h2>
378 *
379 * <p>
380 * JUnit tests, including ones written in Java, may be run by specifying
381 * <code>-j classname</code>, where the classname is a valid JUnit class
382 * such as a TestCase, TestSuite, or a class implementing a static suite()
383 * method returning a TestSuite. </p>
384 * <p>
385 * To use this option you must include a JUnit jar file on your classpath.
386 * </p>
387 *
388 * @author Bill Venners
389 * @author George Berger
390 * @author Josh Cough
391 */
392object Runner {
393
394  private val RUNNER_JFRAME_START_X: Int = 150
395  private val RUNNER_JFRAME_START_Y: Int = 100
396
397  //                     TO
398  // We always include a PassFailReporter on runs in order to determine
399  // whether or not all tests passed.
400  //
401  // The thread that calls Runner.run() will either start a GUI, if a graphic
402  // reporter was requested, or just run the tests itself. If a GUI is started,
403  // an event handler thread will get going, and it will start a RunnerThread,
404  // which will actually do the running. The GUI can repeatedly start RunnerThreads
405  // and RerunnerThreads, until the GUI is closed. If -c is specified, that means
406  // the tests should be run concurrently, which in turn means a Distributor will
407  // be passed to the execute method of the Suites, which will in turn populate
408  // it with its nested suites instead of executing them directly in the same
409  // thread. The Distributor works in conjunction with a pool of threads that
410  // will take suites out of the distributor queue and execute them. The DispatchReporter
411  // will serialize all reports via an actor, which because that actor uses receive
412  // not react, will have its own thread. So the DispatchReporter actor's thread will
413  // be the one that actually invokes TestFailed, RunAborted, etc., on this PassFailReporter.
414  // The thread that invoked Runner.run(), will be the one that calls allTestsPassed.
415  //
416  // The thread that invoked Runner.run() will be the one to instantiate the PassFailReporter
417  // and in its primary constructor acquire the single semaphore permit. This permit will
418  // only be released by the DispatchReporter's actor thread when a runAborted, runStopped,
419  // or runCompleted is invoked. allTestsPassed will block until it can reacquire the lone
420  // semaphore permit. Thus, a PassFailReporter can just be used for one run, then it is
421  // spent. A new PassFailReporter is therefore created each time the Runner.run() method is invoked.
422  //
423  private class PassFailReporter extends Reporter {
424
425    @volatile private var failedAbortedOrStopped = false
426    private val runDoneSemaphore = new Semaphore(1)
427    runDoneSemaphore.acquire()
428
429    override def apply(event: Event) {
430      event match {
431        case _: TestFailed =>
432          failedAbortedOrStopped = true
433
434        case _: RunAborted =>
435          failedAbortedOrStopped = true
436          runDoneSemaphore.release()
437
438        case _: SuiteAborted =>
439          failedAbortedOrStopped = true
440
441        case _: RunStopped =>
442          failedAbortedOrStopped = true
443          runDoneSemaphore.release()
444
445        case _: RunCompleted =>
446          runDoneSemaphore.release()
447
448        case _ =>
449      }
450    }
451
452    def allTestsPassed = {
453      runDoneSemaphore.acquire()
454      !failedAbortedOrStopped
455    }
456  }
457
458  // TODO: I don't think I'm enforcing that properties can't start with "org.scalatest"
459  // TODO: I don't think I'm handling rejecting multiple -f/-r with the same arg. -f fred.txt -f fred.txt should
460  // fail, as should -r MyReporter -r MyReporter. I'm failing on -o -o, -g -g, and -e -e, but the error messages
461  // could indeed be nicer.
462  /**
463   * Runs a suite of tests, with optional GUI. See the main documentation for this singleton object for the details.
464   */
465  def main(args: Array[String]) {
466    val result = runOptionallyWithPassFailReporter(args, true)
467
468    if (result)
469      exit(0)
470    else
471      exit(1)
472  }
473
474  /**
475   * Runs a suite of tests, with optional GUI. See the main documentation for this singleton object for the details.
476   * The difference between this method and <code>main</code> is simply that this method will block until the run
477   * has completed, aborted, or been stopped, and return <code>true</code> if all tests executed and passed. In other
478   * words, if any test fails, or if any suite aborts, or if the run aborts or is stopped, this method will
479   * return <code>false</code>. This value is used, for example, by the ScalaTest ant task to determine whether
480   * to continue the build if <code>haltOnFailure</code> is set to <code>true</code>.
481   *
482   * @return true if all tests were executed and passed.
483   */
484  def run(args: Array[String]): Boolean = {
485    runOptionallyWithPassFailReporter(args, true)
486  }
487
488  private def runOptionallyWithPassFailReporter(args: Array[String], runWithPassFailReporter: Boolean): Boolean = {
489
490    checkArgsForValidity(args) match {
491      case Some(s) => {
492        println(s)
493        exit(1)
494      }
495      case None =>
496    }
497
498    val (
499      runpathArgsList,
500      reporterArgsList,
501      suiteArgsList,
502      junitArgsList,
503      propertiesArgsList,
504      includesArgsList,
505      excludesArgsList,
506      concurrentList,
507      membersOnlyArgsList,
508      wildcardArgsList,
509      testNGArgsList
510    ) = parseArgs(args)
511
512    val fullReporterConfigurations: ReporterConfigurations =
513      if (reporterArgsList.isEmpty)
514        // If no reporters specified, just give them a graphic reporter
515        new ReporterConfigurations(Some(GraphicReporterConfiguration(Set())), Nil, Nil, None, None, Nil, Nil)
516      else
517        parseReporterArgsIntoConfigurations(reporterArgsList)
518
519    val suitesList: List[String] = parseSuiteArgsIntoNameStrings(suiteArgsList, "-s")
520    val junitsList: List[String] = parseSuiteArgsIntoNameStrings(junitArgsList, "-j")
521    val runpathList: List[String] = parseRunpathArgIntoList(runpathArgsList)
522    val propertiesMap: Map[String, String] = parsePropertiesArgsIntoMap(propertiesArgsList)
523    val tagsToInclude: Set[String] = parseCompoundArgIntoSet(includesArgsList, "-n")
524    val tagsToExclude: Set[String] = parseCompoundArgIntoSet(excludesArgsList, "-l")
525    val concurrent: Boolean = !concurrentList.isEmpty
526    val numThreads: Int = parseConcurrentNumArg(concurrentList)
527    val membersOnlyList: List[String] = parseSuiteArgsIntoNameStrings(membersOnlyArgsList, "-m")
528    val wildcardList: List[String] = parseSuiteArgsIntoNameStrings(wildcardArgsList, "-w")
529    val testNGList: List[String] = parseSuiteArgsIntoNameStrings(testNGArgsList, "-t")
530
531    val filter = Filter(if (tagsToInclude.isEmpty) None else Some(tagsToInclude), tagsToExclude)
532
533    // If there's a graphic reporter, we need to leave it out of
534    // reporterSpecs, because we want to pass all reporterSpecs except
535    // the graphic reporter's to the RunnerJFrame (because RunnerJFrame *is*
536    // the graphic reporter).
537    val reporterConfigs: ReporterConfigurations =
538      fullReporterConfigurations.graphicReporterConfiguration match {
539        case None => fullReporterConfigurations
540        case Some(grs) => {
541          new ReporterConfigurations(
542            None,
543            fullReporterConfigurations.fileReporterConfigurationList,
544            fullReporterConfigurations.xmlReporterConfigurationList,
545            fullReporterConfigurations.standardOutReporterConfiguration,
546            fullReporterConfigurations.standardErrReporterConfiguration,
547            fullReporterConfigurations.htmlReporterConfigurationList,
548            fullReporterConfigurations.customReporterConfigurationList
549          )
550        }
551      }
552
553    val passFailReporter = if (runWithPassFailReporter) Some(new PassFailReporter) else None
554
555    fullReporterConfigurations.graphicReporterConfiguration match {
556      case Some(GraphicReporterConfiguration(configSet)) => {
557        val graphicEventsToPresent: Set[EventToPresent] = EventToPresent.allEventsToPresent filter
558          (if (configSet.contains(FilterTestStarting)) {_ != PresentTestStarting} else etp => true) filter
559          (if (configSet.contains(FilterTestSucceeded)) {_ != PresentTestSucceeded} else etp => true) filter
560          (if (configSet.contains(FilterTestIgnored)) {_ != PresentTestIgnored} else etp => true) filter
561          (if (configSet.contains(FilterTestPending)) {_ != PresentTestPending} else etp => true) filter
562          (if (configSet.contains(FilterSuiteStarting)) {_ != PresentSuiteStarting} else etp => true) filter
563          (if (configSet.contains(FilterSuiteCompleted)) {_ != PresentSuiteCompleted} else etp => true) filter
564          (if (configSet.contains(FilterInfoProvided)) {_ != PresentInfoProvided} else etp => true)
565
566        val abq = new ArrayBlockingQueue[RunnerJFrame](1)
567        usingEventDispatchThread {
568          val rjf = new RunnerJFrame(graphicEventsToPresent, reporterConfigs, suitesList, junitsList, runpathList,
569            filter, propertiesMap, concurrent, membersOnlyList, wildcardList, testNGList, passFailReporter, numThreads)
570          rjf.setLocation(RUNNER_JFRAME_START_X, RUNNER_JFRAME_START_Y)
571          rjf.setVisible(true)
572          rjf.prepUIForRunning()
573          rjf.runFromGUI()
574          abq.put(rjf)
575        }
576        // To get the Ant task to work, the main thread needs to block until
577        // The GUI window exits.
578        val rjf = abq.take()
579        rjf.blockUntilWindowClosed()
580      }
581      case None => { // Run the test without a GUI
582        withClassLoaderAndDispatchReporter(runpathList, reporterConfigs, None, passFailReporter) {
583          (loader, dispatchReporter) => {
584            doRunRunRunDaDoRunRun(dispatchReporter, suitesList, junitsList, new Stopper {}, filter,
585                propertiesMap, concurrent, membersOnlyList, wildcardList, testNGList, runpathList, loader, new RunDoneListener {}, 1, numThreads)
586          }
587        }
588      }
589    }
590
591    passFailReporter match {
592      case Some(pfr) => pfr.allTestsPassed
593      case None => false
594    }
595  }
596
597  // Returns an Option[String]. Some is an error message. None means no error.
598  private[scalatest] def checkArgsForValidity(args: Array[String]) = {
599
600    val lb = new ListBuffer[String]
601    val it = args.iterator
602    while (it.hasNext) {
603      val s = it.next
604      // Style advice
605      // If it is multiple else ifs, then make it symetrical. If one needs an open curly brace, put it on all
606      // If an if just has another if, a compound statement, go ahead and put the open curly brace's around the outer one
607      if (s.startsWith("-p") || s.startsWith("-f") || s.startsWith("-u") || s.startsWith("-h") || s.startsWith("-r") || s.startsWith("-n") || s.startsWith("-x") || s.startsWith("-l") || s.startsWith("-s") || s.startsWith("-j") || s.startsWith("-m") || s.startsWith("-w") || s.startsWith("-t")) {
608        if (it.hasNext)
609          it.next
610      }
611      else if (!s.startsWith("-D") && !s.startsWith("-g") && !s.startsWith("-o") && !s.startsWith("-e") && !s.startsWith("-c")) {
612        lb += s
613      }
614    }
615    val argsList = lb.toList
616    if (argsList.length != 0)
617      Some("Unrecognized argument" + (if (argsList.isEmpty) ": " else "s: ") + argsList.mkString("", ", ", "."))
618    else
619      None
620  }
621
622  //
623  // Examines concurrent option arg to see if it contains an optional numeric
624  // value representing the number of threads to use, e.g. -c10 for 10 threads.
625  //
626  // It's possible for user to specify the -c option multiple times on the
627  // command line, although it isn't particularly useful.  This method scans
628  // through multiples until it finds one with a number appended and uses
629  // that.  If none have a number it just returns 0.
630  //
631  private[scalatest] def parseConcurrentNumArg(concurrentList: List[String]):
632  Int = {
633    val opt = concurrentList.find(_.matches("-c\\d+"))
634
635    opt match {
636      case Some(arg) => arg.replace("-c", "").toInt
637      case None      => 0
638    }
639  }
640
641  private[scalatest] def parseArgs(args: Array[String]) = {
642
643    val runpath = new ListBuffer[String]()
644    val reporters = new ListBuffer[String]()
645    val suites = new ListBuffer[String]()
646    val junits = new ListBuffer[String]()
647    val props = new ListBuffer[String]()
648    val includes = new ListBuffer[String]()
649    val excludes = new ListBuffer[String]()
650    val concurrent = new ListBuffer[String]()
651    val membersOnly = new ListBuffer[String]()
652    val wildcard = new ListBuffer[String]()
653    val testNGXMLFiles = new ListBuffer[String]()
654
655    val it = args.iterator
656    while (it.hasNext) {
657
658      val s = it.next
659
660      if (s.startsWith("-D")) {
661         props += s
662      }
663      else if (s.startsWith("-p")) {
664        runpath += s
665        if (it.hasNext)
666          runpath += it.next
667      }
668      else if (s.startsWith("-g")) {
669        reporters += s
670      }
671      else if (s.startsWith("-o")) {
672        reporters += s
673      }
674      else if (s.startsWith("-e")) {
675        reporters += s
676      }
677      else if (s.startsWith("-f")) {
678        reporters += s
679        if (it.hasNext)
680          reporters += it.next
681      }
682      else if (s.startsWith("-u")) {
683        reporters += s
684        if (it.hasNext)
685          reporters += it.next
686      }
687      else if (s.startsWith("-h")) {
688        reporters += s
689        if (it.hasNext)
690          reporters += it.next
691      }
692      else if (s.startsWith("-n")) {
693        includes += s
694        if (it.hasNext)
695          includes += it.next
696      }
697      else if (s.startsWith("-x")) {
698        System.err.println(Resources("dashXDeprecated"))
699        excludes += s.replace("-x", "-l")
700        if (it.hasNext)
701          excludes += it.next
702      }
703      else if (s.startsWith("-l")) {
704        excludes += s
705        if (it.hasNext)
706          excludes += it.next
707      }
708      else if (s.startsWith("-r")) {
709
710        reporters += s
711        if (it.hasNext)
712          reporters += it.next
713      }
714      else if (s.startsWith("-s")) {
715
716        suites += s
717        if (it.hasNext)
718          suites += it.next
719      }
720      else if (s.startsWith("-j")) {
721
722        junits += s
723        if (it.hasNext)
724          junits += it.next
725      }
726      else if (s.startsWith("-m")) {
727
728        membersOnly += s
729        if (it.hasNext)
730          membersOnly += it.next
731      }
732      else if (s.startsWith("-w")) {
733
734        wildcard += s
735        if (it.hasNext)
736          wildcard += it.next
737      }
738      else if (s.startsWith("-c")) {
739
740        concurrent += s
741      }
742      else if (s.startsWith("-t")) {
743
744        testNGXMLFiles += s
745        if (it.hasNext)
746          testNGXMLFiles += it.next
747      }
748      else {
749        throw new IllegalArgumentException("Unrecognized argument: " + s)
750      }
751    }
752
753    (
754      runpath.toList,
755      reporters.toList,
756      suites.toList,
757      junits.toList,
758      props.toList,
759      includes.toList,
760      excludes.toList,
761      concurrent.toList,
762      membersOnly.toList,
763      wildcard.toList,
764      testNGXMLFiles.toList
765    )
766  }
767
768  /**
769   * Returns a possibly empty ConfigSet containing configuration
770   * objects specified in the passed reporterArg. Configuration
771   * options are specified immediately following
772   * the reporter option, as in:
773   *
774   * -oFA
775   *
776   * If no configuration options are specified, this method returns an
777   * empty ConfigSet. This method never returns null.
778   */
779  private def parseConfigSet(reporterArg: String): Set[ReporterConfigParam] = {
780
781    if (reporterArg == null)
782      throw new NullPointerException("reporterArg was null")
783
784    if (reporterArg.length < 2)
785      throw new IllegalArgumentException("reporterArg < 2")
786
787    // The reporterArg passed includes the initial -, as in "-oFI",
788    // so the first config param will be at index 2
789    val configString = reporterArg.substring(2)
790    val it = configString.iterator
791    var set = Set[ReporterConfigParam]()
792    while (it.hasNext)
793      it.next match {
794        case 'Y' =>  throw new IllegalArgumentException("Use of Y was deprecated in ScalaTest 1.0 and removed in 1.5. Please check the Scaladoc documentation of org.scalatest.Runner for information on valid Reporter config parameters.")
795        case 'Z' => throw new IllegalArgumentException("Use of Z was deprecated in ScalaTest 1.0 and removed in 1.5. Please check the Scaladoc documentation of org.scalatest.Runner for information on valid Reporter config parameters.")
796        case 'T' => // Use for Dots
797        case 'U' => throw new IllegalArgumentException("Use of U was deprecated in ScalaTest 1.0 and removed in 1.5. Please check the Scaladoc documentation of org.scalatest.Runner for information on valid Reporter config parameters.")
798        case 'P' =>throw new IllegalArgumentException("Use of P was deprecated in ScalaTest 1.0 and removed in 1.5. Please check the Scaladoc documentation of org.scalatest.Runner for information on valid Reporter config parameters.")
799        case 'B' =>throw new IllegalArgumentException("Use of B was deprecated in ScalaTest 1.0 and removed in 1.5. Please check the Scaladoc documentation of org.scalatest.Runner for information on valid Reporter config parameters.")
800        case 'I' =>throw new IllegalArgumentException("Use of I was deprecated in ScalaTest 1.0 and removed in 1.5. Please check the Scaladoc documentation of org.scalatest.Runner for information on valid Reporter config parameters.")
801        // case 'S' => // Use for Short Stack Traces
802        case 'A' =>throw new IllegalArgumentException("Use of A was deprecated in ScalaTest 1.0 and removed in 1.5. Please check the Scaladoc documentation of org.scalatest.Runner for information on valid Reporter config parameters.")
803        case 'R' =>throw new IllegalArgumentException("Use of R was deprecated in ScalaTest 1.0 and removed in 1.5. Please check the Scaladoc documentation of org.scalatest.Runner for information on valid Reporter config parameters.")
804        case 'G' =>throw new IllegalArgumentException("Use of G was deprecated in ScalaTest 1.0 and removed in 1.5. Please check the Scaladoc documentation of org.scalatest.Runner for information on valid Reporter config parameters.")
805        case 'N' => set += FilterTestStarting
806        case 'C' => set += FilterTestSucceeded
807        case 'X' => set += FilterTestIgnored
808        case 'E' => set += FilterTestPending
809        case 'H' => set += FilterSuiteStarting
810        case 'L' => set += FilterSuiteCompleted
811        case 'O' => set += FilterInfoProvided
812        case 'W' => set += PresentWithoutColor
813        case 'F' => set += PresentFullStackTraces
814        case 'S' => set += PresentShortStackTraces
815        case 'D' => set += PresentAllDurations
816        case c: Char => {
817
818          // this should be moved to the checker, and just throw an exception here with a debug message. Or allow a MatchError.
819          val msg1 = Resources("invalidConfigOption", String.valueOf(c)) + '\n'
820          val msg2 =  Resources("probarg", reporterArg) + '\n'
821
822          throw new IllegalArgumentException(msg1 + msg2)
823        }
824      }
825    set
826  }
827
828  private[scalatest] def parseReporterArgsIntoConfigurations(args: List[String]) = {
829    //
830    // Checks to see if any args are smaller than two characters in length.
831    // Allows a one-character arg if it's a directory-name parameter, to
832    // permit use of "." for example.
833    //
834    def argTooShort(args: List[String]): Boolean = {
835      args match {
836        case Nil => false
837
838        case "-u" :: directory :: list => argTooShort(list)
839
840        case x :: list =>
841          if (x.length < 2) true
842          else              argTooShort(list)
843      }
844    }
845
846    if (args == null)
847      throw new NullPointerException("args was null")
848
849    if (args.exists(_ == null))
850      throw new NullPointerException("an arg String was null")
851
852    if (argTooShort(args)) // TODO: check and print out a user friendly message for this
853      throw new IllegalArgumentException("an arg String was less than 2 in length: " + args)
854
855    for (dashX <- List("-g", "-o", "-e")) {
856      if (args.toList.count(_.startsWith(dashX)) > 1) // TODO: also check and print a user friendly message for this
857        throw new IllegalArgumentException("Only one " + dashX + " allowed")
858    }
859
860    // TODO: also check and print a user friendly message for this
861    // again here, i had to skip some things, so I had to use an iterator.
862    val it = args.iterator
863    while (it.hasNext)
864      it.next.take(2).toString match {
865        case "-g" =>
866        case "-o" =>
867        case "-e" =>
868        case "-f" =>
869          if (it.hasNext)
870            it.next // scroll past the filename
871          else
872            throw new IllegalArgumentException("-f needs to be followed by a file name arg: ")
873        case "-u" =>
874          if (it.hasNext) {
875            val directory = it.next
876            if (!(new File(directory).isDirectory))
877              throw new IllegalArgumentException(
878                "arg for -u option is not a directory [" + directory + "]")
879            else {}
880          }
881          else {
882            throw new IllegalArgumentException("-u needs to be followed by a directory name arg: ")
883          }
884        case "-h" =>
885          if (it.hasNext)
886            it.next // scroll past the filename
887          else
888            throw new IllegalArgumentException("-h needs to be followed by a file name arg: ")
889        case "-r" =>
890          if (it.hasNext)
891            it.next // scroll past the reporter class
892          else
893            throw new IllegalArgumentException("-r needs to be followed by a reporter class name arg: ")
894        case arg: String =>
895          throw new IllegalArgumentException("An arg started with an invalid character string: " + arg)
896      }
897
898    val graphicReporterConfigurationOption =
899      args.find(arg => arg.startsWith("-g")) match {
900        case Some(dashGString) =>
901          val configSet = parseConfigSet(dashGString)
902          if (configSet.contains(PresentShortStackTraces))
903            throw new IllegalArgumentException("Cannot specify an S (present short stack traces) configuration parameter for the graphic reporter (because it shows them anyway): " + dashGString)
904          if (configSet.contains(PresentFullStackTraces))
905            throw new IllegalArgumentException("Cannot specify an F (present full stack traces) configuration parameter for the graphic reporter (because it shows them anyway): " + dashGString)
906          if (configSet.contains(PresentWithoutColor))
907            throw new IllegalArgumentException("Cannot specify a W (present without color) configuration parameter for the graphic reporter: " + dashGString)
908          if (configSet.contains(PresentAllDurations))
909            throw new IllegalArgumentException("Cannot specify a D (present all durations) configuration parameter for the graphic reporter (because it shows them all anyway): " + dashGString)
910          Some(new GraphicReporterConfiguration(configSet))
911        case None => None
912      }
913
914    def buildFileReporterConfigurationList(args: List[String]) = {
915      val it = args.iterator
916      val lb = new ListBuffer[FileReporterConfiguration]
917      while (it.hasNext) {
918        val arg = it.next
919        if (arg.startsWith("-f"))
920          lb += new FileReporterConfiguration(parseConfigSet(arg), it.next)
921      }
922      lb.toList
923    }
924    val fileReporterConfigurationList = buildFileReporterConfigurationList(args)
925
926    def buildXmlReporterConfigurationList(args: List[String]) = {
927      val it = args.iterator
928      val lb = new ListBuffer[XmlReporterConfiguration]
929      while (it.hasNext) {
930        val arg = it.next
931        if (arg.startsWith("-u"))
932          lb += new XmlReporterConfiguration(Set[ReporterConfigParam](),
933                                             it.next)
934      }
935      lb.toList
936    }
937    val xmlReporterConfigurationList = buildXmlReporterConfigurationList(args)
938
939    def buildHtmlReporterConfigurationList(args: List[String]) = {
940      val it = args.iterator
941      val lb = new ListBuffer[HtmlReporterConfiguration]
942      while (it.hasNext) {
943        val arg = it.next
944        if (arg.startsWith("-h"))
945          lb += new HtmlReporterConfiguration(parseConfigSet(arg), it.next)
946      }
947      lb.toList
948    }
949    val htmlReporterConfigurationList = buildHtmlReporterConfigurationList(args)
950
951    val standardOutReporterConfigurationOption =
952      args.find(arg => arg.startsWith("-o")) match {
953        case Some(dashOString) => Some(new StandardOutReporterConfiguration(parseConfigSet(dashOString)))
954        case None => None
955      }
956
957    val standardErrReporterConfigurationOption =
958      args.find(arg => arg.startsWith("-e")) match {
959        case Some(dashEString) => Some(new StandardErrReporterConfiguration(parseConfigSet(dashEString)))
960        case None => None
961      }
962
963    def buildCustomReporterConfigurationList(args: List[String]) = {
964      val it = args.iterator
965      val lb = new ListBuffer[CustomReporterConfiguration]
966      while (it.hasNext) {
967        val arg = it.next
968        if (arg.startsWith("-r")) {
969          val dashRString = arg
970          val customReporterClassName = it.next
971          val configSet = parseConfigSet(dashRString)
972          if (configSet.contains(PresentShortStackTraces))
973            throw new IllegalArgumentException("Cannot specify an S (present short stack traces) configuration parameter for a custom reporter: " + dashRString + " " + customReporterClassName)
974          if (configSet.contains(PresentFullStackTraces))
975            throw new IllegalArgumentException("Cannot specify an F (present full stack traces) configuration parameter for a custom reporter: " + dashRString + " " + customReporterClassName)
976          if (configSet.contains(PresentWithoutColor))
977            throw new IllegalArgumentException("Cannot specify a W (without color) configuration parameter for a custom reporter: " + dashRString + " " + customReporterClassName)
978          if (configSet.contains(PresentAllDurations))
979            throw new IllegalArgumentException("Cannot specify a D (present all durations) configuration parameter for a custom reporter: " + dashRString + " " + customReporterClassName)
980          lb += new CustomReporterConfiguration(configSet, customReporterClassName)
981        }
982      }
983      lb.toList
984    }
985    val customReporterConfigurationList = buildCustomReporterConfigurationList(args)
986
987    // Here instead of one loop, i go through the loop several times.
988    new ReporterConfigurations(
989      graphicReporterConfigurationOption,
990      fileReporterConfigurationList,
991      xmlReporterConfigurationList,
992      standardOutReporterConfigurationOption,
993      standardErrReporterConfigurationOption,
994      htmlReporterConfigurationList,
995      customReporterConfigurationList
996    )
997  }
998
999  // Used to parse -s, -j, -m, and -w args, one of which will be passed as a String as dashArg
1000  private[scalatest] def parseSuiteArgsIntoNameStrings(args: List[String], dashArg: String) = {
1001
1002    if (args == null)
1003      throw new NullPointerException("args was null")
1004
1005    if (args.exists(_ == null))
1006      throw new NullPointerException("an arg String was null")
1007
1008    if (dashArg != "-j" && dashArg != "-s" && dashArg != "-w" && dashArg != "-m" && dashArg != "-t")
1009      throw new NullPointerException("dashArg invalid: " + dashArg)
1010
1011    val lb = new ListBuffer[String]
1012    val it = args.iterator
1013    while (it.hasNext) {
1014      val dashS = it.next
1015      if (dashS != dashArg)
1016        throw new IllegalArgumentException("Every other element, starting with the first, must be -s")
1017      if (it.hasNext) {
1018        val suiteName = it.next
1019        if (!suiteName.startsWith("-"))
1020          lb += suiteName
1021        else
1022          throw new IllegalArgumentException("Expecting a Suite class name to follow -s, but got: " + suiteName)
1023      }
1024      else
1025        throw new IllegalArgumentException("Last element must be a Suite class name, not a -s.")
1026    }
1027    lb.toList
1028  }
1029
1030  private[scalatest] def parseCompoundArgIntoSet(args: List[String], expectedDashArg: String): Set[String] =
1031      Set() ++ parseCompoundArgIntoList(args, expectedDashArg)
1032
1033  private[scalatest] def parseRunpathArgIntoList(args: List[String]): List[String] = parseCompoundArgIntoList(args, "-p")
1034
1035  private[scalatest] def parseCompoundArgIntoList(args: List[String], expectedDashArg: String): List[String] = {
1036
1037    if (args == null)
1038      throw new NullPointerException("args was null")
1039
1040    if (args.exists(_ == null))
1041      throw new NullPointerException("an arg String was null")
1042
1043    if (args.length == 0) {
1044      List()
1045    }
1046    else if (args.length == 2) {
1047      val dashArg = args(0)
1048      val runpathArg = args(1)
1049
1050      if (dashArg != expectedDashArg)
1051        throw new IllegalArgumentException("First arg must be " + expectedDashArg + ", but was: " + dashArg)
1052
1053      if (runpathArg.trim.isEmpty)
1054        throw new IllegalArgumentException("The runpath string must actually include some non-whitespace characters.")
1055
1056      splitPath(runpathArg)
1057    }
1058    else {
1059      throw new IllegalArgumentException("Runpath must be either zero or two args: " + args)
1060    }
1061  }
1062
1063  //
1064  // Splits a space-delimited path into its component parts.
1065  //
1066  // Spaces within path elements may be escaped with backslashes, e.g.
1067  // "c:\Documents\ And\ Settings c:\Program\ Files"
1068  //
1069  // See comments for isCompleteToken() below for exceptions.
1070  //
1071  private val START_TOKEN_PATTERN = Pattern.compile("""^\s*(.*?)(\s|$)""")
1072  private val FULL_TOKEN_PATTERN  = Pattern.compile("""^\s*(.+?)(((?<=[^\\])\s)|$)""")
1073  private def splitPath(pathArg: String): List[String] = {
1074    val path = pathArg.trim
1075
1076    if (path.isEmpty) Nil
1077    else {
1078      val startMatcher = START_TOKEN_PATTERN.matcher(path)
1079
1080      if (!startMatcher.find())
1081        throw new RuntimeException("unexpected startMatcher path [" +
1082                                   path + "]")
1083      val token = startMatcher.group(1)
1084
1085      if (isCompleteToken(token)) {
1086        token :: splitPath(path.substring(startMatcher.end))
1087      }
1088      else {
1089        val fullMatcher = FULL_TOKEN_PATTERN.matcher(path)
1090
1091        if (!fullMatcher.find())
1092          throw new RuntimeException("unexpected fullMatcher path [" +
1093                                     path + "]")
1094        val fullToken = fullMatcher.group(1).replaceAll("""\\(\s)""", "$1")
1095
1096        fullToken :: splitPath(path.substring(fullMatcher.end))
1097      }
1098    }
1099  }
1100
1101  //
1102  // Determines whether specified token is complete or partial.
1103  //
1104  // Tokens are considered partial if they end with a backslash, since
1105  // backslash is used to escape spaces that would otherwise be
1106  // treated as delimiters within the path string.
1107  //
1108  // Exceptions are cases where the token ends in a backslash
1109  // but is still considered a complete token because it constitutes
1110  // a valid representation of a root directory on a windows system,
1111  // e.g. "c:\" or just "\".
1112  //
1113  private val ROOT_DIR_PATTERN = Pattern.compile("""(?i)\\|[a-z]:\\""")
1114  private def isCompleteToken(token: String): Boolean = {
1115    val matcher = ROOT_DIR_PATTERN.matcher(token)
1116
1117    matcher.matches() || (token(token.length - 1) != '\\')
1118  }
1119
1120  private[scalatest] def parsePropertiesArgsIntoMap(args: List[String]) = {
1121
1122    if (args == null)
1123      throw new NullPointerException("args was null")
1124
1125    if (args.exists(_ == null))
1126      throw new NullPointerException("an arg String was null")
1127
1128    if (args.exists(_.indexOf('=') == -1))
1129      throw new IllegalArgumentException("A -D arg does not contain an equals sign.")
1130
1131    if (args.exists(!_.startsWith("-D")))
1132      throw new IllegalArgumentException("A spice arg does not start with -D.")
1133
1134    if (args.exists(_.indexOf('=') == 2))
1135      throw new IllegalArgumentException("A spice arg does not have a key to the left of the equals sign.")
1136
1137    if (args.exists(arg => arg.indexOf('=') == arg.length - 1))
1138      throw new IllegalArgumentException("A spice arg does not have a value to the right of the equals sign.")
1139
1140    val tuples = for (arg <- args) yield {
1141      val keyValue = arg.substring(2) // Cut off the -D at the beginning
1142      val equalsPos = keyValue.indexOf('=')
1143      val key = keyValue.substring(0, equalsPos)
1144      val value = keyValue.substring(equalsPos + 1)
1145      (key, value)
1146    }
1147
1148    scala.collection.immutable.Map() ++ tuples
1149  }
1150
1151  // For debugging.
1152/*
1153  private[scalatest] def printOpts(opt: EventToPresent.Set32) {
1154    if (opt.contains(EventToPresent.PresentRunStarting))
1155      println("PresentRunStarting")
1156    if (opt.contains(EventToPresent.PresentTestStarting))
1157      println("PresentTestStarting")
1158    if (opt.contains(EventToPresent.PresentTestSucceeded))
1159      println("PresentTestSucceeded")
1160    if (opt.contains(EventToPresent.PresentTestFailed))
1161      println("PresentTestFailed")
1162    if (opt.contains(EventToPresent.PresentTestIgnored))
1163      println("PresentTestIgnored")
1164    if (opt.contains(EventToPresent.PresentSuiteStarting))
1165      println("PresentSuiteStarting")
1166    if (opt.contains(EventToPresent.PresentSuiteCompleted))
1167      println("PresentSuiteCompleted")
1168    if (opt.contains(EventToPresent.PresentSuiteAborted))
1169      println("PresentSuiteAborted")
1170    if (opt.contains(EventToPresent.PresentInfoProvided))
1171      println("PresentInfoProvided")
1172    if (opt.contains(EventToPresent.PresentRunStopped))
1173      println("PresentRunStopped")
1174    if (opt.contains(EventToPresent.PresentRunCompleted))
1175      println("PresentRunCompleted")
1176    if (opt.contains(EventToPresent.PresentRunAborted))
1177      println("PresentRunAborted")
1178  }
1179*/
1180
1181  private def configSetMinusNonFilterParams(configSet: Set[ReporterConfigParam]) =
1182    (((configSet - PresentShortStackTraces) - PresentFullStackTraces) - PresentWithoutColor) - PresentAllDurations
1183
1184  private[scalatest] def getDispatchReporter(reporterSpecs: ReporterConfigurations, graphicReporter: Option[Reporter], passFailReporter: Option[Reporter], loader: ClassLoader) = {
1185
1186    def getReporterFromConfiguration(configuration: ReporterConfiguration): Reporter =
1187
1188      configuration match {
1189        case StandardOutReporterConfiguration(configSet) =>
1190          if (configSetMinusNonFilterParams(configSet).isEmpty)
1191            new StandardOutReporter(
1192              configSet.contains(PresentAllDurations),
1193              !configSet.contains(PresentWithoutColor),
1194              configSet.contains(PresentShortStackTraces) || configSet.contains(PresentFullStackTraces),
1195              configSet.contains(PresentFullStackTraces) // If they say both S and F, F overrules
1196            )
1197          else
1198            new FilterReporter(
1199              new StandardOutReporter(
1200                configSet.contains(PresentAllDurations),
1201                !configSet.contains(PresentWithoutColor),
1202                configSet.contains(PresentShortStackTraces) || configSet.contains(PresentFullStackTraces),
1203                configSet.contains(PresentFullStackTraces) // If they say both S and F, F overrules
1204              ),
1205              configSet
1206            )
1207
1208      case StandardErrReporterConfiguration(configSet) =>
1209        if (configSetMinusNonFilterParams(configSet).isEmpty)
1210          new StandardErrReporter(
1211            configSet.contains(PresentAllDurations),
1212            !configSet.contains(PresentWithoutColor),
1213            configSet.contains(PresentShortStackTraces) || configSet.contains(PresentFullStackTraces),
1214            configSet.contains(PresentFullStackTraces) // If they say both S and F, F overrules
1215          )
1216        else
1217          new FilterReporter(
1218            new StandardErrReporter(
1219              configSet.contains(PresentAllDurations),
1220              !configSet.contains(PresentWithoutColor),
1221              configSet.contains(PresentShortStackTraces) || configSet.contains(PresentFullStackTraces),
1222              configSet.contains(PresentFullStackTraces) // If they say both S and F, F overrules
1223            ),
1224            configSet
1225          )
1226
1227      case FileReporterConfiguration(configSet, fileName) =>
1228        if (configSetMinusNonFilterParams(configSet).isEmpty)
1229          new FileReporter(
1230            fileName,
1231            configSet.contains(PresentAllDurations),
1232            !configSet.contains(PresentWithoutColor),
1233            configSet.contains(PresentShortStackTraces) || configSet.contains(PresentFullStackTraces),
1234            configSet.contains(PresentFullStackTraces) // If they say both S and F, F overrules
1235          )
1236        else
1237          new FilterReporter(
1238            new FileReporter(
1239              fileName,
1240              configSet.contains(PresentAllDurations),
1241              !configSet.contains(PresentWithoutColor),
1242              configSet.contains(PresentShortStackTraces) || configSet.contains(PresentFullStackTraces),
1243              configSet.contains(PresentFullStackTraces) // If they say both S and F, F overrules
1244            ),
1245            configSet
1246          )
1247
1248      case XmlReporterConfiguration(configSet, directory) =>
1249        new XmlReporter(directory)
1250
1251        case HtmlReporterConfiguration(configSet, fileName) =>
1252          if (configSetMinusNonFilterParams(configSet).isEmpty)
1253            new HtmlReporter(
1254              fileName,
1255              configSet.contains(PresentAllDurations),
1256              !configSet.contains(PresentWithoutColor),
1257              configSet.contains(PresentShortStackTraces) || configSet.contains(PresentFullStackTraces),
1258              configSet.contains(PresentFullStackTraces) // If they say both S and F, F overrules
1259            )
1260          else
1261            new FilterReporter(
1262              new HtmlReporter(
1263                fileName,
1264                configSet.contains(PresentAllDurations),
1265                !configSet.contains(PresentWithoutColor),
1266                configSet.contains(PresentShortStackTraces) || configSet.contains(PresentFullStackTraces),
1267                configSet.contains(PresentFullStackTraces) // If they say both S and F, F overrules
1268              ),
1269              configSet
1270            )
1271
1272      case CustomReporterConfiguration(configSet, reporterClassName) => {
1273        val customReporter = getCustomReporter(reporterClassName, loader, "-r... " + reporterClassName)
1274        if (configSet.isEmpty)
1275          customReporter
1276        else
1277          new FilterReporter(customReporter, configSet)
1278      }
1279      case GraphicReporterConfiguration(configSet) => throw new RuntimeException("Should never happen.")
1280    }
1281
1282    val reporterSeq =
1283      (for (spec <- reporterSpecs)
1284        yield getReporterFromConfiguration(spec))
1285
1286    val almostFullReporterList: List[Reporter] =
1287      graphicReporter match {
1288        case None => reporterSeq.toList
1289        case Some(gRep) => gRep :: reporterSeq.toList
1290      }
1291
1292    val fullReporterList: List[Reporter] =
1293      passFailReporter match {
1294        case Some(pfr) => pfr :: almostFullReporterList
1295        case None => almostFullReporterList
1296      }
1297
1298    new DispatchReporter(fullReporterList)
1299  }
1300
1301  private def getCustomReporter(reporterClassName: String, loader: ClassLoader, argString: String): Reporter = {
1302    try {
1303      val reporterClass: java.lang.Class[_] = loader.loadClass(reporterClassName)
1304      reporterClass.newInstance.asInstanceOf[Reporter]
1305    }    // Could probably catch ClassCastException too
1306    catch {
1307      case e: ClassNotFoundException => {
1308
1309        val msg1 = Resources("cantLoadReporterClass", reporterClassName)
1310        val msg2 = Resources("probarg", argString)
1311        val msg = msg1 + "\n" + msg2
1312
1313        val iae = new IllegalArgumentException(msg)
1314        iae.initCause(e)
1315        throw iae
1316      }
1317      case e: InstantiationException => {
1318
1319        val msg1 = Resources("cantInstantiateReporter", reporterClassName)
1320        val msg2 = Resources("probarg", argString)
1321        val msg = msg1 + "\n" + msg2
1322
1323        val iae = new IllegalArgumentException(msg)
1324        iae.initCause(e)
1325        throw iae
1326      }
1327      case e: IllegalAccessException => {
1328
1329        val msg1 = Resources("cantInstantiateReporter", reporterClassName)
1330        val msg2 = Resources("probarg", argString)
1331        val msg = msg1 + "\n" + msg2
1332
1333        val iae = new IllegalArgumentException(msg)
1334        iae.initCause(e)
1335        throw iae
1336      }
1337    }
1338  }
1339
1340  private[scalatest] def doRunRunRunDaDoRunRun(
1341    dispatch: DispatchReporter,
1342    suitesList: List[String],
1343    junitsList: List[String],
1344    stopRequested: Stopper,
1345    filter: Filter,
1346    configMap: Map[String, String],
1347    concurrent: Boolean,
1348    membersOnlyList: List[String],
1349    wildcardList: List[String],
1350    testNGList: List[String],
1351    runpath: List[String],
1352    loader: ClassLoader,
1353    doneListener: RunDoneListener,
1354    runStamp: Int,
1355    numThreads: Int
1356  ) = {
1357
1358    // TODO: add more, and to RunnerThread too
1359    if (dispatch == null)
1360      throw new NullPointerException
1361    if (suitesList == null)
1362      throw new NullPointerException
1363    if (junitsList == null)
1364      throw new NullPointerException
1365    if (stopRequested == null)
1366      throw new NullPointerException
1367    if (filter == null)
1368      throw new NullPointerException
1369    if (configMap == null)
1370      throw new NullPointerException
1371    if (membersOnlyList == null)
1372      throw new NullPointerException
1373    if (wildcardList == null)
1374      throw new NullPointerException
1375    if (runpath == null)
1376      throw new NullPointerException
1377    if (loader == null)
1378      throw new NullPointerException
1379    if (doneListener == null)
1380      throw new NullPointerException
1381
1382    val tagsToInclude =
1383      filter.tagsToInclude match {
1384        case None => Set[String]()
1385        case Some(tti) => tti
1386      }
1387    val tagsToExclude = filter.tagsToExclude
1388
1389    var tracker = new Tracker(new Ordinal(runStamp))
1390
1391    val runStartTime = System.currentTimeMillis
1392
1393    try {
1394      val loadProblemsExist =
1395        try {
1396          val unassignableList = suitesList.filter(className => !classOf[Suite].isAssignableFrom(loader.loadClass(className)))
1397          if (!unassignableList.isEmpty) {
1398            val names = for (className <- unassignableList) yield " " + className
1399            dispatch(RunAborted(tracker.nextOrdinal(), Resources("nonSuite") + names, None))
1400            true
1401          }
1402          else {
1403            false
1404          }
1405        }
1406        catch {
1407          case e: ClassNotFoundException => {
1408            dispatch(RunAborted(tracker.nextOrdinal(), Resources("cannotLoadSuite", e.getMessage), Some(e)))
1409            true
1410          }
1411        }
1412
1413      if (!loadProblemsExist) {
1414        try {
1415          val namedSuiteInstances: List[Suite] =
1416            for (suiteClassName <- suitesList)
1417              yield {
1418                val clazz = loader.loadClass(suiteClassName)
1419                clazz.newInstance.asInstanceOf[Suite]
1420              }
1421
1422          val junitSuiteInstances: List[Suite] =
1423            for (junitClassName <- junitsList)
1424              yield new JUnitWrapperSuite(junitClassName, loader)
1425
1426          val testNGWrapperSuiteList: List[TestNGWrapperSuite] =
1427            if (!testNGList.isEmpty)
1428              List(new TestNGWrapperSuite(testNGList))
1429            else
1430              Nil
1431
1432          val (membersOnlySuiteInstances, wildcardSuiteInstances) = {
1433
1434            val membersOnlyAndBeginsWithListsAreEmpty = membersOnlyList.isEmpty && wildcardList.isEmpty // They didn't specify any -m's or -w's on the command line
1435
1436
1437            // TODO: rename the 'BeginsWith' variables to 'Wildcard' to match the terminology that
1438            // we ended up with on the outside
1439            // TODO: Should SuiteDiscoverHelper be a singleton object?
1440            if (membersOnlyAndBeginsWithListsAreEmpty && (!suitesList.isEmpty || !junitsList.isEmpty)) {
1441              (Nil, Nil) // No DiscoverySuites in this case. Just run Suites named with -s or -j
1442            }
1443            else {
1444              val accessibleSuites = (new SuiteDiscoveryHelper).discoverSuiteNames(runpath, loader)
1445
1446              if (membersOnlyAndBeginsWithListsAreEmpty && suitesList.isEmpty && junitsList.isEmpty) {
1447                // In this case, they didn't specify any -w, -m, -s, or -j on the command line, so the default
1448                // is to run any accessible Suites discovered on the runpath
1449                (Nil, List(new DiscoverySuite("", accessibleSuites, true, loader)))
1450              }
1451              else {
1452                val membersOnlyInstances =
1453                  for (membersOnlyName <- membersOnlyList)
1454                    yield new DiscoverySuite(membersOnlyName, accessibleSuites, false, loader)
1455
1456                val wildcardInstances =
1457                  for (wildcardName <- wildcardList)
1458                    yield new DiscoverySuite(wildcardName, accessibleSuites, true, loader)
1459
1460                (membersOnlyInstances, wildcardInstances)
1461              }
1462            }
1463          }
1464
1465          val suiteInstances: List[Suite] = namedSuiteInstances ::: junitSuiteInstances ::: membersOnlySuiteInstances ::: wildcardSuiteInstances ::: testNGWrapperSuiteList
1466
1467          val testCountList =
1468            for (suite <- suiteInstances)
1469              yield suite.expectedTestCount(filter)
1470
1471          def sumInts(list: List[Int]): Int =
1472            list match {
1473              case Nil => 0
1474              case x :: xs => x + sumInts(xs)
1475            }
1476
1477          val expectedTestCount = sumInts(testCountList)
1478
1479          dispatch(RunStarting(tracker.nextOrdinal(), expectedTestCount, configMap))
1480
1481          if (concurrent) {
1482
1483            // Because some tests may do IO, will create a pool of 2 times the number of processors reported
1484            // by the Runtime's availableProcessors method.
1485            val poolSize =
1486              if (numThreads > 0) numThreads
1487              else Runtime.getRuntime.availableProcessors * 2
1488
1489            val execSvc: ExecutorService = Executors.newFixedThreadPool(poolSize)
1490            try {
1491
1492            if (System.getProperty("org.scalatest.tools.Runner.forever", "false") == "true") {
1493              val distributor = new ConcurrentDistributor(dispatch, stopRequested, filter, configMap, execSvc)
1494              while (true) {
1495                for (suite <- suiteInstances) {
1496                  distributor.apply(suite, tracker.nextTracker())
1497                }
1498                distributor.waitUntilDone()
1499              }
1500            }
1501            else {
1502              val distributor = new ConcurrentDistributor(dispatch, stopRequested, filter, configMap, execSvc)
1503              for (suite <- suiteInstances) {
1504                distributor.apply(suite, tracker.nextTracker())
1505              }
1506              distributor.waitUntilDone()
1507            }
1508            }
1509            finally {
1510              execSvc.shutdown()
1511            }
1512          }
1513          else {
1514            for (suite <- suiteInstances) {
1515              val suiteRunner = new SuiteRunner(suite, dispatch, stopRequested, filter,
1516                  configMap, None, tracker)
1517              suiteRunner.run()
1518            }
1519          }
1520
1521          val duration = System.currentTimeMillis - runStartTime
1522          if (stopRequested()) {
1523            dispatch(RunStopped(tracker.nextOrdinal(), Some(duration)))
1524          }
1525          else {
1526            dispatch(RunCompleted(tracker.nextOrdinal(), Some(duration)))
1527          }
1528        }
1529        catch {
1530          case e: InstantiationException =>
1531            dispatch(RunAborted(tracker.nextOrdinal(), Resources("cannotInstantiateSuite", e.getMessage), Some(e), Some(System.currentTimeMillis - runStartTime)))
1532          case e: IllegalAccessException =>
1533            dispatch(RunAborted(tracker.nextOrdinal(), Resources("cannotInstantiateSuite", e.getMessage), Some(e), Some(System.currentTimeMillis - runStartTime)))
1534          case e: NoClassDefFoundError =>
1535            dispatch(RunAborted(tracker.nextOrdinal(), Resources("cannotLoadClass", e.getMessage), Some(e), Some(System.currentTimeMillis - runStartTime)))
1536          case e: Throwable =>
1537            dispatch(RunAborted(tracker.nextOrdinal(), Resources.bigProblems(e), Some(e), Some(System.currentTimeMillis - runStartTime)))
1538        }
1539      }
1540    }
1541    finally {
1542      dispatch.dispatchDisposeAndWaitUntilDone()
1543      doneListener.done()
1544    }
1545  }
1546
1547  private[scalatest] def excludesWithIgnore(excludes: Set[String]) = excludes + "org.scalatest.Ignore"
1548
1549  private[scalatest] def withClassLoaderAndDispatchReporter(runpathList: List[String], reporterSpecs: ReporterConfigurations,
1550      graphicReporter: Option[Reporter], passFailReporter: Option[Reporter])(f: (ClassLoader, DispatchReporter) => Unit): Unit = {
1551
1552    val loader: ClassLoader = getRunpathClassLoader(runpathList)
1553    try {
1554      Thread.currentThread.setContextClassLoader(loader)
1555      try {
1556        val dispatchReporter = getDispatchReporter(reporterSpecs, graphicReporter, passFailReporter, loader)
1557        try {
1558          f(loader, dispatchReporter)
1559        }
1560        finally {
1561          dispatchReporter.dispatchDisposeAndWaitUntilDone()
1562        }
1563      }
1564      catch {
1565        // getDispatchReporter may complete abruptly with an exception, if there is an problem trying to load
1566        // or instantiate a custom reporter class.
1567        case ex: Throwable => {
1568          System.err.println(Resources("bigProblemsMaybeCustomReporter"))
1569          ex.printStackTrace(System.err)
1570        }
1571      }
1572    }
1573    finally {
1574      // eventually call close on the RunpathClassLoader
1575    }
1576  }
1577
1578  private[scalatest] def getRunpathClassLoader(runpathList: List[String]): ClassLoader = {
1579
1580    if (runpathList == null)
1581      throw new NullPointerException
1582    if (runpathList.isEmpty) {
1583      classOf[Suite].getClassLoader // Could this be null technically?
1584    }
1585    else {
1586      val urlsList: List[URL] =
1587        for (raw <- runpathList) yield {
1588          try {
1589            new URL(raw)
1590          }
1591          catch {
1592            case murle: MalformedURLException => {
1593
1594              // Assume they tried to just pass in a file name
1595              val file: File = new File(raw)
1596
1597              // file.toURL may throw MalformedURLException too, but for now
1598              // just let that propagate up.
1599              file.toURI.toURL // If a dir, comes back terminated by a slash
1600            }
1601          }
1602        }
1603
1604      // Here is where the Jini preferred class loader stuff went.
1605
1606      // Tell the URLConnections to not use caching, so that repeated runs and reruns actually work
1607      // on the latest binaries.
1608      for (url <- urlsList) {
1609        try {
1610          url.openConnection.setDefaultUseCaches(false)
1611        }
1612        catch {
1613          case e: IOException => // just ignore these
1614        }
1615      }
1616
1617      new URLClassLoader(urlsList.toArray, classOf[Suite].getClassLoader)
1618    }
1619  }
1620
1621  private[scalatest] def usingEventDispatchThread(f: => Unit): Unit = {
1622    SwingUtilities.invokeLater(
1623      new Runnable() {
1624        def run() {
1625          f
1626        }
1627      }
1628    )
1629  }
1630}
1631