From d06df9a2fe0db102648e9e16d81dab7f0a55c07f Mon Sep 17 00:00:00 2001 From: Mark Harrah Date: Wed, 20 Jan 2010 23:06:10 -0500 Subject: [PATCH] * Integrate Josh's test arguments patch * Use ScalaTest arguments example to test test-interface --- scripted/src/main/scala/SbtHandler.scala | 6 +- src/main/scala/sbt/DefaultProject.scala | 20 ++--- src/main/scala/sbt/IntegrationTesting.scala | 6 +- src/main/scala/sbt/ScalaProject.scala | 89 ++++++++++++------- src/main/scala/sbt/TestFramework.scala | 45 ++++++---- .../tests/arguments/project/build.properties | 2 + .../project/build/ArgumentTest.scala | 7 ++ .../src/test/scala/ArgumentTest.scala | 14 +++ src/sbt-test/tests/arguments/test | 27 ++++++ 9 files changed, 149 insertions(+), 67 deletions(-) create mode 100644 src/sbt-test/tests/arguments/project/build.properties create mode 100644 src/sbt-test/tests/arguments/project/build/ArgumentTest.scala create mode 100644 src/sbt-test/tests/arguments/src/test/scala/ArgumentTest.scala create mode 100644 src/sbt-test/tests/arguments/test diff --git a/scripted/src/main/scala/SbtHandler.scala b/scripted/src/main/scala/SbtHandler.scala index 22d879fee..e7a5942aa 100644 --- a/scripted/src/main/scala/SbtHandler.scala +++ b/scripted/src/main/scala/SbtHandler.scala @@ -11,7 +11,7 @@ final class SbtHandler(directory: File, log: Logger, server: IPC.Server) extends def initialState = newRemote def apply(command: String, arguments: List[String], p: Process): Process = { - send((command :: arguments).mkString(" ")) + send((command :: arguments.map(escape)).mkString(" ")) receive(command + " failed") p } @@ -38,5 +38,9 @@ final class SbtHandler(directory: File, log: Logger, server: IPC.Server) extends catch { case e: java.net.SocketException => error("Remote sbt initialization failed") } p } + // if the argument contains spaces, enclose it in quotes, quoting backslashes and quotes + def escape(argument: String) = + if(argument.contains(" ")) "\"" + argument.replaceAll(q("""\"""), """\\""").replaceAll(q("\""), "\\\"") + "\"" else argument + def q(s: String) = java.util.regex.Pattern.quote(s) // Process("java" :: "-classpath" :: classpath.map(_.getAbsolutePath).mkString(File.pathSeparator) :: "xsbt.boot.Boot" :: ( "<" + server.port) :: Nil) run log } diff --git a/src/main/scala/sbt/DefaultProject.scala b/src/main/scala/sbt/DefaultProject.scala index f5d62213d..c7a1c91c0 100644 --- a/src/main/scala/sbt/DefaultProject.scala +++ b/src/main/scala/sbt/DefaultProject.scala @@ -267,16 +267,16 @@ abstract class BasicScalaProject extends ScalaProject with BasicDependencyProjec protected def docAction = scaladocTask(mainLabel, mainSources, mainDocPath, docClasspath, documentOptions).dependsOn(compile) describedAs DocDescription protected def docTestAction = scaladocTask(testLabel, testSources, testDocPath, docClasspath, documentOptions).dependsOn(testCompile) describedAs TestDocDescription - protected def testAction = defaultTestTask(Nil, testOptions) - protected def testOnlyAction = testQuickMethod(testCompileConditional.analysis, testOptions)((testArgs, options) => { - defaultTestTask(testArgs, options) - }) describedAs(TestOnlyDescription) - protected def testQuickAction = defaultTestQuickMethod(false) describedAs(TestQuickDescription) - protected def testFailedAction = defaultTestQuickMethod(true) describedAs(TestFailedDescription) - protected def defaultTestQuickMethod(failedOnly: Boolean) = - testQuickMethod(testCompileConditional.analysis, testOptions)((args, options) => defaultTestTask(args, quickOptions(failedOnly) ::: options.toList)) - protected def defaultTestTask(testArgs: Seq[String], testOptions: => Seq[TestOption]) = - testTask(testFrameworks, testClasspath, testCompileConditional.analysis, testArgs, testOptions).dependsOn(testCompile, copyResources, copyTestResources) describedAs TestDescription + protected def testAction = defaultTestTask(testOptions) + protected def testOnlyAction = testQuickMethod(testCompileConditional.analysis, testOptions)((options) => { + defaultTestTask(options) + }) describedAs (TestOnlyDescription) + protected def testQuickAction = defaultTestQuickMethod(false) describedAs (TestQuickDescription) + protected def testFailedAction = defaultTestQuickMethod(true) describedAs (TestFailedDescription) + protected def defaultTestQuickMethod(failedOnly: Boolean) = + testQuickMethod(testCompileConditional.analysis, testOptions)(options => defaultTestTask(quickOptions(failedOnly) ::: options.toList)) + protected def defaultTestTask(testOptions: => Seq[TestOption]) = + testTask(testFrameworks, testClasspath, testCompileConditional.analysis, testOptions).dependsOn(testCompile, copyResources, copyTestResources) describedAs TestDescription override def packageToPublishActions: Seq[ManagedTask] = `package` :: Nil diff --git a/src/main/scala/sbt/IntegrationTesting.scala b/src/main/scala/sbt/IntegrationTesting.scala index 5edcb9dce..1957f0b4a 100644 --- a/src/main/scala/sbt/IntegrationTesting.scala +++ b/src/main/scala/sbt/IntegrationTesting.scala @@ -15,8 +15,8 @@ trait IntegrationTesting extends NotNull trait ScalaIntegrationTesting extends IntegrationTesting { self: ScalaProject => - protected def integrationTestTask(frameworks: Seq[TestFramework], classpath: PathFinder, analysis: CompileAnalysis, testArgs: Seq[String], options: => Seq[TestOption]) = - testTask(frameworks, classpath, analysis, testArgs, options) + protected def integrationTestTask(frameworks: Seq[TestFramework], classpath: PathFinder, analysis: CompileAnalysis, options: => Seq[TestOption]) = + testTask(frameworks, classpath, analysis, options) } trait BasicScalaIntegrationTesting extends BasicIntegrationTesting with MavenStyleIntegrationTestPaths @@ -34,7 +34,7 @@ trait BasicIntegrationTesting extends ScalaIntegrationTesting with IntegrationTe val integrationTestCompileConditional = new CompileConditional(integrationTestCompileConfiguration, buildCompiler) - protected def integrationTestAction = integrationTestTask(integrationTestFrameworks, integrationTestClasspath, integrationTestCompileConditional.analysis, Nil, integrationTestOptions) dependsOn integrationTestCompile describedAs IntegrationTestCompileDescription + protected def integrationTestAction = integrationTestTask(integrationTestFrameworks, integrationTestClasspath, integrationTestCompileConditional.analysis, integrationTestOptions) dependsOn integrationTestCompile describedAs IntegrationTestCompileDescription protected def integrationTestCompileAction = integrationTestCompileTask() dependsOn compile describedAs IntegrationTestDescription protected def integrationTestCompileTask() = task{ integrationTestCompileConditional.run } diff --git a/src/main/scala/sbt/ScalaProject.scala b/src/main/scala/sbt/ScalaProject.scala index b7e6405d5..4657013e5 100644 --- a/src/main/scala/sbt/ScalaProject.scala +++ b/src/main/scala/sbt/ScalaProject.scala @@ -67,7 +67,14 @@ trait ScalaProject extends SimpleScalaProject with FileTasks with MultiTaskProje case class ExcludeTests(tests: Iterable[String]) extends TestOption case class TestListeners(listeners: Iterable[TestReportListener]) extends TestOption case class TestFilter(filterTest: String => Boolean) extends TestOption - case class TestFrameworkArgument(arg: String) extends TestOption + + // args for all frameworks + def TestArgument(args: String*): TestArgument = TestArgument(None, args.toList) + // args for a particular test framework + def TestArgument(tf: TestFramework, args: String*): TestArgument = TestArgument(Some(tf), args.toList) + + // None means apply to all, Some(tf) means apply to a particular framework only. + case class TestArgument(framework: Option[TestFramework], args: List[String]) extends TestOption case class JarManifest(m: Manifest) extends PackageOption { @@ -150,14 +157,13 @@ trait ScalaProject extends SimpleScalaProject with FileTasks with MultiTaskProje def copyTask(sources: PathFinder, destinationDirectory: Path): Task = task { FileUtilities.copy(sources.get, destinationDirectory, log).left.toOption } - def testTask(frameworks: Seq[TestFramework], classpath: PathFinder, analysis: CompileAnalysis, testArgs: Seq[String], options: TestOption*): Task = - testTask(frameworks, classpath, analysis, testArgs, options) - def testTask(frameworks: Seq[TestFramework], classpath: PathFinder, analysis: CompileAnalysis, - testArgs: Seq[String], options: => Seq[TestOption]): Task = + def testTask(frameworks: Seq[TestFramework], classpath: PathFinder, analysis: CompileAnalysis, options: TestOption*): Task = + testTask(frameworks, classpath, analysis, options) + def testTask(frameworks: Seq[TestFramework], classpath: PathFinder, analysis: CompileAnalysis, options: => Seq[TestOption]): Task = { def work = { - val (begin, work, end) = testTasks(frameworks, classpath, analysis, testArgs, options) + val (begin, work, end) = testTasks(frameworks, classpath, analysis, options) val beginTasks = begin.map(toTask).toSeq // test setup tasks val workTasks = work.map(w => toTask(w) dependsOn(beginTasks : _*)) // the actual tests val endTasks = end.map(toTask).toSeq // tasks that perform test cleanup and are run regardless of success of tests @@ -251,43 +257,58 @@ trait ScalaProject extends SimpleScalaProject with FileTasks with MultiTaskProje } } protected def incrementImpl(v: BasicVersion): Version = v.incrementMicro - protected def testTasks(frameworks: Seq[TestFramework], classpath: PathFinder, analysis: CompileAnalysis, testArgs: Seq[String], - options: => Seq[TestOption]) = { + protected def testTasks(frameworks: Seq[TestFramework], classpath: PathFinder, analysis: CompileAnalysis, options: => Seq[TestOption]) = + { import scala.collection.mutable.HashSet + import scala.collection.mutable.Map - val testFilters = new ListBuffer[String => Boolean] - val excludeTestsSet = new HashSet[String] - val setup, cleanup = new ListBuffer[() => Option[String]] - val testListeners = new ListBuffer[TestReportListener] + val testFilters = new ListBuffer[String => Boolean] + val excludeTestsSet = new HashSet[String] + val setup, cleanup = new ListBuffer[() => Option[String]] + val testListeners = new ListBuffer[TestReportListener] + val testArgsByFramework = Map[TestFramework, ListBuffer[String]]() + def frameworkArgs(framework: TestFramework): ListBuffer[String] = + testArgsByFramework.getOrElseUpdate(framework, new ListBuffer[String]) - for(option <- options) + for(option <- options) + { + option match { - option match - { - case TestFilter(include) => testFilters += include - case ExcludeTests(exclude) => excludeTestsSet ++= exclude - case TestListeners(listeners) => testListeners ++= listeners - case TestSetup(setupFunction) => setup += setupFunction - case TestCleanup(cleanupFunction) => cleanup += cleanupFunction - } + case TestFilter(include) => testFilters += include + case ExcludeTests(exclude) => excludeTestsSet ++= exclude + case TestListeners(listeners) => testListeners ++= listeners + case TestSetup(setupFunction) => setup += setupFunction + case TestCleanup(cleanupFunction) => cleanup += cleanupFunction + /** + * There are two cases here. + * The first handles TestArguments in the project file, which + * might have a TestFramework specified. + * The second handles arguments to be applied to all test frameworks. + * -- arguments from the project file that didnt have a framework specified + * -- command line arguments (ex: test-only someClass -- someArg) + * (currently, command line args must be passed to all frameworks) + */ + case TestArgument(Some(framework), args) => frameworkArgs(framework) ++= args + case TestArgument(None, args) => frameworks.foreach { framework => frameworkArgs(framework) ++= args.toList } } + } - if(excludeTestsSet.size > 0 && log.atLevel(Level.Debug)) - { - log.debug("Excluding tests: ") - excludeTestsSet.foreach(test => log.debug("\t" + test)) - } - def includeTest(test: TestDefinition) = !excludeTestsSet.contains(test.testClassName) && testFilters.forall(filter => filter(test.testClassName)) - val tests = HashSet.empty[TestDefinition] ++ analysis.allTests.filter(includeTest) - TestFramework.testTasks(frameworks, classpath.get, buildScalaInstance.loader, tests.toSeq, log, testListeners.readOnly, false, setup.readOnly, cleanup.readOnly, testArgs) + if(excludeTestsSet.size > 0 && log.atLevel(Level.Debug)) + { + log.debug("Excluding tests: ") + excludeTestsSet.foreach(test => log.debug("\t" + test)) + } + def includeTest(test: TestDefinition) = !excludeTestsSet.contains(test.testClassName) && testFilters.forall(filter => filter(test.testClassName)) + val tests = HashSet.empty[TestDefinition] ++ analysis.allTests.filter(includeTest) + TestFramework.testTasks(frameworks, classpath.get, buildScalaInstance.loader, tests.toSeq, log, + testListeners.readOnly, false, setup.readOnly, cleanup.readOnly, testArgsByFramework) } private def flatten[T](i: Iterable[Iterable[T]]) = i.flatMap(x => x) - protected def testQuickMethod(testAnalysis: CompileAnalysis, options: => Seq[TestOption])(toRun: (Seq[String],Seq[TestOption]) => Task) = { + protected def testQuickMethod(testAnalysis: CompileAnalysis, options: => Seq[TestOption])(toRun: (Seq[TestOption]) => Task) = { val analysis = testAnalysis.allTests.map(_.testClassName).toList - multiTask(analysis) { (args,includeFunction) => { - toRun(args, TestFilter(includeFunction) :: options.toList) - } + multiTask(analysis) { (args, includeFunction) => + toRun(TestArgument(args:_*) :: TestFilter(includeFunction) :: options.toList) } } @@ -351,7 +372,7 @@ object ScalaProject } trait MultiTaskProject extends Project { - def multiTask(allTests: => List[String])(run: (List[String], (String => Boolean)) => Task): MethodTask = { + def multiTask(allTests: => List[String])(run: (Seq[String], String => Boolean) => Task): MethodTask = { task { tests => diff --git a/src/main/scala/sbt/TestFramework.scala b/src/main/scala/sbt/TestFramework.scala index c872dce96..ea7303b8e 100644 --- a/src/main/scala/sbt/TestFramework.scala +++ b/src/main/scala/sbt/TestFramework.scala @@ -34,6 +34,7 @@ final class TestRunner(framework: Framework, loader: ClassLoader, listeners: Seq private[this] val delegate = framework.testRunner(loader, listeners.flatMap(_.contentLogger).toArray) final def run(testDefinition: TestDefinition, args: Seq[String]): Result.Value = { + log.debug("Running " + testDefinition + " with arguments " + args.mkString(", ")) val testClass = testDefinition.testClassName def runTest() = { @@ -87,28 +88,34 @@ object TestFramework private[sbt] def safeForeach[T](it: Iterable[T], log: Logger)(f: T => Unit): Unit = it.foreach(i => Control.trapAndLog(log){ f(i) } ) - import scala.collection.{Map, Set} - + import scala.collection.{immutable, Map, Set} + def testTasks(frameworks: Seq[TestFramework], - classpath: Iterable[Path], - scalaLoader: ClassLoader, - tests: Seq[TestDefinition], - log: Logger, - listeners: Seq[TestReportListener], - endErrorsEnabled: Boolean, - setup: Iterable[() => Option[String]], - cleanup: Iterable[() => Option[String]], - testArgs: Seq[String]): (Iterable[NamedTestTask], Iterable[NamedTestTask], Iterable[NamedTestTask]) = + classpath: Iterable[Path], + scalaLoader: ClassLoader, + tests: Seq[TestDefinition], + log: Logger, + listeners: Seq[TestReportListener], + endErrorsEnabled: Boolean, + setup: Iterable[() => Option[String]], + cleanup: Iterable[() => Option[String]], + testArgsByFramework: Map[TestFramework, Seq[String]]): + (Iterable[NamedTestTask], Iterable[NamedTestTask], Iterable[NamedTestTask]) = { val loader = createTestLoader(classpath, scalaLoader) - val rawFrameworks = frameworks.flatMap(_.create(loader, log)) - val mappedTests = testMap(rawFrameworks, tests) + val arguments = immutable.Map() ++ + ( for(framework <- frameworks; created <- framework.create(loader, log)) yield + (created, testArgsByFramework.getOrElse(framework, Nil)) ) + + val mappedTests = testMap(arguments.keys.toList, tests, arguments) if(mappedTests.isEmpty) (new NamedTestTask(TestStartName, None) :: Nil, Nil, new NamedTestTask(TestFinishName, { log.info("No tests to run."); None }) :: Nil ) else - createTestTasks(loader, mappedTests, log, listeners, endErrorsEnabled, setup, cleanup, testArgs) + createTestTasks(loader, mappedTests, log, listeners, endErrorsEnabled, setup, cleanup) } - private def testMap(frameworks: Seq[Framework], tests: Seq[TestDefinition]): Map[Framework, Set[TestDefinition]] = + + private def testMap(frameworks: Seq[Framework], tests: Seq[TestDefinition], args: Map[Framework, Seq[String]]): + immutable.Map[Framework, (Set[TestDefinition], Seq[String])] = { import scala.collection.mutable.{HashMap, HashSet, Set} val map = new HashMap[Framework, Set[TestDefinition]] @@ -127,14 +134,14 @@ object TestFramework } if(!frameworks.isEmpty) assignTests() - wrap.Wrappers.readOnly(map) + (immutable.Map() ++ map) transform { (framework, tests) => (tests, args(framework)) } } private def createTasks(work: Iterable[() => Option[String]], baseName: String) = work.toList.zipWithIndex.map{ case (work, index) => new NamedTestTask(baseName + " " + (index+1), work()) } - private def createTestTasks(loader: ClassLoader, tests: Map[Framework, Set[TestDefinition]], log: Logger, + private def createTestTasks(loader: ClassLoader, tests: Map[Framework, (Set[TestDefinition], Seq[String])], log: Logger, listeners: Seq[TestReportListener], endErrorsEnabled: Boolean, setup: Iterable[() => Option[String]], - cleanup: Iterable[() => Option[String]], testArgs: Seq[String]) = + cleanup: Iterable[() => Option[String]]) = { val testsListeners = listeners.filter(_.isInstanceOf[TestsListener]).map(_.asInstanceOf[TestsListener]) def foreachListenerSafe(f: TestsListener => Unit): Unit = safeForeach(testsListeners, log)(f) @@ -148,7 +155,7 @@ object TestFramework } val startTask = new NamedTestTask(TestStartName, {foreachListenerSafe(_.doInit); None}) :: createTasks(setup, "Test setup") val testTasks = - tests flatMap { case (framework, testDefinitions) => + tests flatMap { case (framework, (testDefinitions, testArgs)) => val runner = new TestRunner(framework, loader, listeners, log) for(testDefinition <- testDefinitions) yield diff --git a/src/sbt-test/tests/arguments/project/build.properties b/src/sbt-test/tests/arguments/project/build.properties new file mode 100644 index 000000000..d164f7135 --- /dev/null +++ b/src/sbt-test/tests/arguments/project/build.properties @@ -0,0 +1,2 @@ +project.name=Test Arguments Test +project.version=1.0 \ No newline at end of file diff --git a/src/sbt-test/tests/arguments/project/build/ArgumentTest.scala b/src/sbt-test/tests/arguments/project/build/ArgumentTest.scala new file mode 100644 index 000000000..3ba7d55d1 --- /dev/null +++ b/src/sbt-test/tests/arguments/project/build/ArgumentTest.scala @@ -0,0 +1,7 @@ +import sbt._ + +class ArgumentTest(info: ProjectInfo) extends DefaultProject(info) +{ + val snap = ScalaToolsSnapshots + val st = "org.scalatest" % "scalatest" % "1.0.1-for-scala-2.8.0.Beta1-RC7-with-test-interfaces-0.3-SNAPSHOT" +} \ No newline at end of file diff --git a/src/sbt-test/tests/arguments/src/test/scala/ArgumentTest.scala b/src/sbt-test/tests/arguments/src/test/scala/ArgumentTest.scala new file mode 100644 index 000000000..695a14d04 --- /dev/null +++ b/src/sbt-test/tests/arguments/src/test/scala/ArgumentTest.scala @@ -0,0 +1,14 @@ + +import org.scalatest.fixture.FixtureFunSuite +import org.scalatest.Tag + +class ArgumentTest extends FixtureFunSuite{ + type FixtureParam = Map[String,Any] + override def withFixture(test: OneArgTest) { + test(test.configMap) + } + test("1", Tag("test1")){ conf => error("error #1") } + test("2", Tag("test2")){ conf => () } + test("3", Tag("test3")){ conf => () } + test("4", Tag("test4")){ conf => error("error #4") } +} \ No newline at end of file diff --git a/src/sbt-test/tests/arguments/test b/src/sbt-test/tests/arguments/test new file mode 100644 index 000000000..15262a804 --- /dev/null +++ b/src/sbt-test/tests/arguments/test @@ -0,0 +1,27 @@ +> ++2.8.0.Beta1-RC7 +> update + +# should fail because it should run all tests, some of which are expected to fail (1 and 4) +-> test-only ArgumentTest + +# should fail because it should run the test tagged 'test1', which should fail +-> test-only ArgumentTest -- -n test1 + +# should succeed because it should only run the test tagged 'test2', which should succeed +> test-only ArgumentTest -- -n test2 + +# should succeed because it should only run the test tagged 'test3', which should succeed +> test-only ArgumentTest -- -n test3 + +# should fail because it should run the test tagged 'test4', which should fail +-> test-only ArgumentTest -- -n test4 + +# should succeed because it should only run the tests tagged 'test2' or 'test3', both of which should succeed +> test-only ArgumentTest -- -n "test2 test3" + +# these should fail because they run at least one failed test +-> test-only ArgumentTest -- -n "test2 test4" +-> test-only ArgumentTest -- -n "test1 test2 test3" +-> test-only ArgumentTest -- -n "test2 test3 test4" +-> test-only ArgumentTest -- -n "test1 test2 test3 test4" +-> test-only ArgumentTest -- -n "test1 test3"