How to add custom resource directory to test classpath without copying the files inside? - sbt

When I run my tests, the contents of my special resources directory special-resources are copied to the target/classes directory. I have something like this
unmanagedResourceDirectories in Compile += baseDirectory.value / "special-resources",
But I do not want to copy these resources into the target directory, but I want them to be on the classpath of forked java processes (e.g. for testing).
I have tried using
unmanagedClasspath in Compile += baseDirectory.value / "special-resources",
but the resources are not available.
How can I add a resource directory to the classpath without having to copy the files? Or, alternatively, how can I setup sbt to not copy resources to the target directory?

To have the contents of the special-resources directory included in the classpath for tests and runMain task, do the following:
unmanagedClasspath in Test += baseDirectory.value / "special-resources"
unmanagedClasspath in (Compile, runMain) += baseDirectory.value / "special-resources"
Check that the setting is set properly with show:
> show test:unmanagedClasspath
[info] List(Attributed(C:\dev\sandbox\runtime-assembly\special-resources))
With the following Specs2 tests I'm convinced the setup worked fine:
import org.specs2.mutable._
class HelloWorldSpec extends Specification {
"Hello world" should {
"find the file on classpath" in {
val text = io.Source.fromInputStream(getClass.getResourceAsStream("hello.txt")).mkString
text must have size(11)
}
}
}
hello.txt is in the special-resources directory with a hello world string inside.
> ; clean ; test
[success] Total time: 0 s, completed 2014-08-06 20:00:02
[info] Updating {file:/C:/dev/sandbox/runtime-assembly/}runtime-assembly...
[info] Resolving org.jacoco#org.jacoco.agent;0.7.1.201405082137 ...
[info] Done updating.
[info] Compiling 1 Scala source to C:\dev\sandbox\runtime-assembly\target\scala-2.10\test-classes...
[info] HelloWorldSpec
[info]
[info] Hello world should
[info] + find the file on classpath
[info]
[info] Total for specification HelloWorldSpec
[info] Finished in 17 ms
[info] 1 example, 0 failure, 0 error
[info] Passed: Total 1, Failed 0, Errors 0, Passed 1
And build.sbt:
unmanagedClasspath in Test += baseDirectory.value / "special-resources"
libraryDependencies += "org.specs2" %% "specs2" % "2.4" % "test"
fork in Test := true

Related

sbt: Invoking tests after other tasks

I have a multi-project build, and want to run tests after starting two Docker containers. This is my custom task:
runTestsWithDocker := Def.taskDyn {
startDirectoryServer.value
val containerId = buildOrStartTestDatabase.value
Def.task {
(test in Test).value
sLog.value.info("running inside dynamic task")
containerId
}
}.value
As you can see from the output below, the Docker containers are started, and the log message is written from the dynamic task. However, there's no test output (and the build executes far too quickly for the tests to have run).
> runTestsWithDocker
[info] logging into ECR registry 123456789012.dkr.ecr.us-east-1.amazonaws.com
[info] checking repository for image container1:1.2.3-1200
[info] successfully logged-in to ECR registry 123456789012.dkr.ecr.us-east-1.amazonaws.com
[info] DockerSupport: pulling 123456789012.dkr.ecr.us-east-1.amazonaws.com/container2:latest
[info] DockerSupport: docker run -d -p 389:389 123456789012.dkr.ecr.us-east-1.amazonaws.com/container2:latest
[info] container ID: 80d16a268c6e13dd810f8c271ca8778fc8eaa6835f2d0640fa62d032ff052345
[info] image already exists; no need to build
[info] DockerSupport: pulling 123456789012.dkr.ecr.us-east-1.amazonaws.com/container1:1.2.3-1200
[info] DockerSupport: docker run -d -p 5432:5432 123456789012.dkr.ecr.us-east-1.amazonaws.com/container1:1.2.3-1200
[info] container ID: 2de559b0737e69d61b1234567890123bd123456789012d382ba8ffa40e0480cf
[info] Updating {file:/home/ubuntu/Workspace/mybuild/}mybuild...
[info] Resolving jline#jline;2.12.1 ...
[info] Done updating.
[info] running inside dynamic task
[success] Total time: 2 s, completed Jun 5, 2019 9:05:20 PM
I'm assuming that my scope is incorrect, and that I need to refer to test in some other scope, but I have no idea what that might be (I've tried Compile and ThisBuild as random stabs in the dark).
I've also seen (test in Test).result.value from other questions in SO. Thinking that maybe the test task was doing something non-standard I tried it, but with the same (non) result.
Lastly, I'm running SBT 0.13.16, so any convincing argument (as in a bug report) that it's a problem with that version would make me upgrade sooner than planned (my current goal is to refactor the build then upgrade).
Update: here's the output from inspect. It doesn't show the dependency on test, but I'm assuming that's because it's invoked from a dynamic task.
> inspect runTestsWithDocker
[info] Task: java.lang.String
[info] Description:
[info] Runs the test suite, after starting the LDAP server and running/initializing the test database
[info] Provided by:
[info] {file:/home/ubuntu/Workspace/mybuild/}mybuild/*:runTestsWithDocker
[info] Defined at:
[info] /home/ubuntu/Workspace/mybuild/build.sbt:597
[info] Dependencies:
[info] mybuild/*:buildOrStartTestDatabase
[info] mybuild/*:startDirectoryServer
[info] mybuild/*:settingsData
[info] Reverse dependencies:
[info] mybuild/*:publishTestDatabase
[info] Delegates:
[info] mybuild/*:runTestsWithDocker
[info] {.}/*:runTestsWithDocker
[info] */*:runTestsWithDocker
Update: if I specify a single sub-project, it correctly runs the tasks in that sub-project.
runTestsWithDocker := Def.taskDyn {
startDirectoryServer.value
val containerId = buildOrStartTestDatabase.value
Def.task {
(test in (subproject,Test)).result.value
containerId
}
}.value
So it looks like maybe the root project isn't aggregating? We're relying on the "default root" project, so I think my next change will be to create an explicit root project.
It turned out that the default root project was not in fact "aggregat[ing] all other projects in the build." Once I created this project and explicitly aggregated the other sub-projects under it, I was able to specify my task like so:
runTestsWithDocker := Def.taskDyn {
startDirectoryServer.value
val containerId = buildOrStartTestDatabase.value
Def.task {
(test in (root,Test)).result.value
containerId
}
}.value
:shrug:

Assign name to root project while keeping the default aggregation

I have a fairly large sbt project (about 30 sub-projects). From what I've figured out, sbt will use the name of the root directory as the name of the root project, if none is declared explicitly in build.sbt. Depending on where the project is checked out, e.g. in a CI environment, that name may change. I'm currently using sbt 1.2.8.
My issue is that I would like to assign a stable name to the root project so that I can e.g. run all tests using sbt root/test [0], leveraging the default aggregation of the root project over all sub-projects. The only way I have found so far to assign a name to the root project is by explicitly declaring the project. But this will disable the default aggregation.
Is there a way to assign a name to the root project that will keep the default aggregation over all sub-projects? Or is there another way to access the root project on the command line without relying on it's name?
[0]: The default project is changed by the build.sbt using onLoad in Global := (Command.process("project ...", _)) compose (onLoad in Global).value. So just running sbt test won't work.
Here is a potential solution without having to explicitly refer to the root project.
Given the following project structure consisting of root project, and sub-projects core and util
build.sbt
core
project
src
target
util
and the following build definition in build.sbt
lazy val util = (project in file("util"))
lazy val core = (project in file("core"))
onLoad in Global := { Command.process("project util", _: State) } compose (onLoad in Global).value
ThisBuild / libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.5" % Test
we could run tests from all projects whilst being in any particular sub-project by defining a custom task testAll which evaluates test using inAnyProject scope filter
val testAll = taskKey[Unit]("Run tests in all projects whilst being in any particular sub-project")
ThisBuild / testAll := Def.taskDyn {
(Test / test).all(ScopeFilter(inAnyProject))
}.value
Now executing sbt will load util sub-project by default, nevertheless testAll should run all tests from all projects:
sbt:util> testAll
[info] RootSpec:
[info] The Root object
[info] - should say root hello
[info] UtilSpec:
[info] The Util object
[info] - should say util hello
[info] CoreSpec:
[info] The Core object
[info] - should say core hello
[info] Run completed in 349 milliseconds.
[info] Total number of tests run: 1
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 1, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.
[info] Run completed in 309 milliseconds.
[info] Total number of tests run: 1
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 1, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.
[info] Run completed in 403 milliseconds.
[info] Total number of tests run: 1
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 1, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.
[success] Total time: 1 s, completed 11-Apr-2019 16:29:26
sbt:util>
where RootSpec, CoreSpec, and UtilsSpec are at
src/test/scala/example/RootSpec.scala
core/src/test/scala/example/CoreSpec.scala
util/src/test/scala/example/UtilSpec.scala

How to develop sbt plugin in multi-project build with projects that use it?

Is it possible to build an sbt plugin in a multi-project setup and use that plugin in some other sub-project of the same multi-project?
For example:
root/
+ mySbtPlugin/
+ myProject/
+ project/
+ plugins.sbt // Uses `mySbtPlugin`
It is not possible since the only way to define plugins for a single or multi-module project is via project (meta)build. I was tricked again to have a solution for you when I set up the sandbox environment with the layout you described.
sbt allows project (meta)project to be in the root directory only. No other project directories are processed and become part of the build definition.
That's why your best (and only) bet is to have the multi-module build for myProject and mySbtPlugin to ease your development, and enable the plugin only for these project(s) you really want to (be careful with auto-plugins, though).
project/plugins.sbt
lazy val root = (project in file(".")) dependsOn sbtNonamePlugin
lazy val sbtNonamePlugin = ProjectRef(file("../sbt-noname"), "sbt-noname")
addSbtPlugin("pl.japila" % "sbt-noname" % "1.0")
build.sbt
lazy val `my-project`, `sbt-noname` = project
sbt-noname/build.sbt
sbtPlugin := true
name := "sbt-noname"
organization := "pl.japila"
version := "1.0"
sbt-noname/src/main/scala/sbtnoname/Plugin.scala
package sbtnoname
import sbt._
import plugins._
object Plugin extends AutoPlugin {
override def trigger = allRequirements
override val projectSettings: Seq[Setting[_]] = inConfig(Test)(baseNonameSettings)
lazy val sayHello = taskKey[Unit]("Say hello")
lazy val baseNonameSettings: Seq[sbt.Def.Setting[_]] = Seq(
sayHello := {
println("I'm the plugin to say hello")
}
)
}
With the files above, run sbt.
> about
[info] This is sbt 0.13.6-SNAPSHOT
[info] The current project is {file:/Users/jacek/sandbox/multi-plugin/}my-project 0.1-SNAPSHOT
[info] The current project is built against Scala 2.10.4
[info] Available Plugins: sbt.plugins.IvyPlugin, sbt.plugins.JvmPlugin, sbt.plugins.CorePlugin, sbt.plugins.JUnitXmlReportPlugin, sbtnoname.Plugin, com.timushev.sbt.updates.UpdatesPlugin
[info] sbt, sbt plugins, and build definitions are using Scala 2.10.4
> projects
[info] In file:/Users/jacek/sandbox/multi-plugin/
[info] * multi-plugin
[info] my-project
[info] sbt-noname
> plugins
In file:/Users/jacek/sandbox/multi-plugin/
sbt.plugins.IvyPlugin: enabled in multi-plugin, sbt-noname, my-project
sbt.plugins.JvmPlugin: enabled in multi-plugin, sbt-noname, my-project
sbt.plugins.CorePlugin: enabled in multi-plugin, sbt-noname, my-project
sbt.plugins.JUnitXmlReportPlugin: enabled in multi-plugin, sbt-noname, my-project
sbtnoname.Plugin: enabled in multi-plugin, sbt-noname, my-project
> my-project/test:sayHello
I'm the plugin to say hello
[success] Total time: 0 s, completed Jun 15, 2014 3:49:50 PM

How to exclude files in a custom clean task?

I'm working on a custom TaskKey that makes a clean that keeps a folder in target directory - it's a database that I don't want to fill everytime.
So I tried something like that:
lazy val cleanOutput = taskKey[Unit]("Prints 'Hello World'")
cleanOutput := clean.value
cleanKeepFiles in cleanOutput <+= target { target => target / "database" }
It seems that the statement in cleanOutput is not taken in consideration.
Even when I do just the following it doesn't work:
cleanKeepFiles in clean <+= target { target => target / "database" }
But the following works:
cleanKeepFiles <+= target { target => target / "database" }
Why is the difference?
There are keys, which may have defined values in different scopes (project, configuration or task).
A key can be defined, even if it's not used in any scope, as well as a key can be given a value in any scope. The later doesn't mean that the value will be used by a specific task. It means you can reuse keys declared by sbt for your purposes.
What you do you declare a new taskKey. You define your task to call clean. You then define cleanKeepFiles in the scope of your new task to equal to whatever the value was before, plus your database directory in the target.
The value is set correctly, but the clean task will not look for it in the scope of your task.
You can verify it:
> show cleanOutput::cleanKeepFiles
[info] List(/home/lpiepiora/Desktop/sbt/stack-overflow/q-24020437/target/.history, /home/lpiepiora/Desktop/sbt/stack-overflow/q-24020437/target/database)
Additionally you can check:
> inspect *:cleanKeepFiles
[info] Setting: scala.collection.Seq[java.io.File] = List(/home/lpiepiora/Desktop/sbt/stack-overflow/q-24020437/target/.history)
[info] Description:
[info] Files to keep during a clean.
[info] Provided by:
[info] {file:/home/lpiepiora/Desktop/sbt/stack-overflow/q-24020437/}q-24020437/*:cleanKeepFiles
[info] Defined at:
[info] (sbt.Defaults) Defaults.scala:278
[info] Dependencies:
[info] *:history
[info] Reverse dependencies:
[info] *:clean
[info] Delegates:
[info] *:cleanKeepFiles
[info] {.}/*:cleanKeepFiles
[info] */*:cleanKeepFiles
[info] Related:
[info] *:cleanOutput::cleanKeepFiles
You can also see that sbt knows, that you have set it in the scope *:cleanOutput::cleanKeepFiles, it is just not using it.
Where it will look for it?. You can check it by inspecting the clean task.
> inspect clean
[info] Task: Unit
[info] Description:
[info] Deletes files produced by the build, such as generated sources, compiled classes, and task caches.
// next lines are important
[info] Dependencies:
[info] *:cleanKeepFiles
[info] *:cleanFiles
You can see that one of the dependencies is *:cleanKeepFiles, the * means Global configuration. This means that the clean task will look for the setting in that scope. You can change your setting to:
cleanKeepFiles += target.value / "database"
This will set it in the correct scope used by the clean task.
Edit
There is a doClean function which you can reuse. Given that, you can define your clean task like this:
val cleanKeepDb = taskKey[Unit]("Cleans folders keeping database")
cleanKeepDb := Defaults.doClean(cleanFiles.value, (cleanKeepFiles in cleanKeepDb).value)
cleanKeepFiles in cleanKeepDb += target.value / "database"

In SBT, how does one `addCompilerPlugin` from Git?

Normally, when not using Git, you can just write:
addCompilerPlugin("something" % "blah" ...)
scalacOptions += "-P:blah:..."
This addCompilerPlugin takes a ModuleID. But here... I've tried adding:
lazy val root = project in file(".") dependsOn
uri("git://github.com/puffnfresh/wartremover.git#master")
to the project/plugins.sbt as well as:
lazy val wartRemover = RootProject(
uri("git://github.com/puffnfresh/wartremover.git#master"))
lazy val root = Project(...).settings(
...
scalacOptions += "-P:wartremover:...",
...
) depends on wartRemover
Both result in:
[error] (root/*:update) sbt.ResolveException: unresolved dependency:
org.brianmckenna#wartremover_2.10.3;0.5-SNAPSHOT: not found
tl;dr The project wartremover has not been published for Scala version 2.10.3. Downgrade yours with the following in build.sbt among the other necessary settings:
scalaVersion := "2.10.2"
Detailed procedure focusing on Scala 2.10.3
The following in build.sbt
addCompilerPlugin("org.brianmckenna" % "wartremover" % "0.5" cross CrossVersion.full)
adds org.brianmckenna:wartremover:0.5:plugin->default(compile) to libraryDependencies.
[sbt-0-13-2]> show libraryDependencies
[info] List(org.scala-lang:scala-library:2.10.3, org.brianmckenna:wartremover:0.5:plugin->default(compile))
So to use a RootProject that points at the project wartremover at GitHub I had to use the following in build.sbt (this is the complete file):
scalacOptions in root += "-P:wartremover:traverser:org.brianmckenna.wartremover.warts.Unsafe"
lazy val root = project in file(".") dependsOn wartRemover % "plugin->default(compile)"
lazy val wartRemover = RootProject(
uri("git://github.com/puffnfresh/wartremover.git#master"))
Since the project wartremover is not published for 2.10.3 I followed the steps below:
Show available projects
[root]> projects
[info] In file:/Users/jacek/sandbox/so/sbt-0.13.2/
[info] * root
[info] In git://github.com/puffnfresh/wartremover.git#master
[info] wartremover
Switch to wartremover and publishLocal it for scalaVersion set to 2.10.3.
[wartremover]> set scalaVersion := "2.10.3"
[info] Defining wartremover/*:scalaVersion
[info] The new value will be used by wartremover/*:allDependencies, wartremover/*:assemblyPackageScala::assemblyDefaultJarName and 12 others.
[info] Run `last` for details.
[info] Reapplying settings...
[info] Set current project to wartremover (in build git://github.com/puffnfresh/wartremover.git#master)
[wartremover]> publishLocal
[info] Packaging /Users/jacek/.sbt/0.13/staging/d6dd3d2e3d818e69943a/wartremover/target/scala-2.10/wartremover_2.10.3-0.6-SNAPSHOT-sources.jar ...
[info] Updating {git://github.com/puffnfresh/wartremover.git#master}wartremover...
...
[info] published ivy to /Users/jacek/.ivy2/local/org.brianmckenna/wartremover_2.10.3/0.6-SNAPSHOT/ivys/ivy.xml
[success] Total time: 7 s, completed Jan 18, 2014 11:34:07 PM
Switch to the project root and do update. It should now work fine.
[wartremover]> project {file:/Users/jacek/sandbox/so/sbt-0.13.2/}
[info] Set current project to root (in build file:/Users/jacek/sandbox/so/sbt-0.13.2/)
[root]> update
[info] Updating {file:/Users/jacek/sandbox/so/sbt-0.13.2/}root...
[info] Resolving org.fusesource.jansi#jansi;1.4 ...
[info] Done updating.
[success] Total time: 0 s, completed Jan 18, 2014 11:36:24 PM
console should work fine now, too.
[root]> console
[info] Starting scala interpreter...
[info]
<console>:5: error: var is disabled
var value: scala.tools.nsc.interpreter.IMain = _
^
Welcome to Scala version 2.10.3 (Java HotSpot(TM) 64-Bit Server VM, Java 1.7.0_45).
Type in expressions to have them evaluated.
Type :help for more information.
scala>
The error message worries me, though. I don't know the plugin and neither do I know how to get rid of it. It also happens when I follow the steps described in Compiler plugin when scalaVersion := "2.10.2" is set in build.sbt (so the compiler plugin's available in the repo).

Resources