4
votes

How does one add a external dependency to a SBT plugin and make it available on both the project and plugin classpath ?:

Specifically I have a simple plugin that should run our TestNG test suites and do some post processing. Here is a simplified version:

import sbt._
import java.util.ArrayList
import Keys._
import org.testng._

object RunTestSuitesPlugin extends Plugin {
  lazy val runTestSuites = TaskKey[Unit]("run-test-suites", "runs TestNG test suites")
  lazy val testSuites = SettingKey[Seq[String]]("test-suites", "list of test suites to run")

  class JavaListWrapper[T](val seq: Seq[T]) {
    def toJavaList = seq.foldLeft(new java.util.ArrayList[T](seq.size)) { (al, e) => al.add(e); al }
  }
  implicit def listToJavaList[T](l: Seq[T]) = new JavaListWrapper(l)

  def runTestSuitesTask = runTestSuites <<= (target, streams, testSuites) map {
    (targetDirectory, taskStream, suites) =>
      import taskStream.log
      log.info("running test suites: " + suites)
      runSuites(suites)
  }

  private def runSuites(testSuites: Seq[String]) = {
    var tester = new TestNG
    tester.setTestSuites(testSuites.toJavaList)
    tester.run()
  }

  def testSuiteSettings = {
    inConfig(Compile)(Seq(
      runTestSuitesTask,
      testSuites := Seq("testsuites/mysuite.xml"),
      libraryDependencies += "org.testng" % "testng" % "5.14"))
  }
}

The problem is that when I add this plugin to a project and run it with run-test-suites then it fails with java.lang.NoClassDefFoundError: org/testng/TestNG even though show full-classpath shows that testng.jar is on the classpath.

So somehow the classpath used when executing the plugin differs from the one in my project, so how do I make a plugin dependency appear in both places ?

2

2 Answers

1
votes

I'll try an answer, but I'm not very familiar with the inner details of sbt.

Normally, the path for the build system (as opposed to your program) is under project, as explained here. That would typically be in a project/plugins.sbt. Sounds right, as there is no reason that the application you develop should be concerned by what libraries your build system uses, nor the other way round.

When your plugin run the application code, that may not be so simple and there could well be classpath/classloader issues. I'm not sure that it will work. Normally, your plugin should implement a testing Framework rather than define its own task. Documentation of testing for sbt is limited.

A testing framework should implement org.scalatools.testing.Framework, in test-interface. Your build will take it into account after you add

testFrameworks += new TestFramework("full.class.name")

When you run the normal test command, it let every framework recognize the test classes it deals with (two criteria available: extending some base class or having some annotation) and run them. The framework run in the build, it is given a class loader to access the application code.

You may have a look at the framework implementation for junit (shipped with sbt). Also there is a TestNG implementation. I don't know it, according to its doc, it is a little bit unorthodox, hopefully it will work for you.

1
votes

The error was fixed by adding TestNG directly to unmanagedJars in Compile in the project that uses the plugin.

I have not found any resources explaining the structure of the SBT class path during plugin execution so any attempt at explaining why this step is necessary will be greatly appreciated.