Add a scala_test rule to Bazel

This implements a scala_test rule analogous to the java_test rule. It uses the standard scalatest library, and adds the corresponding jar(s) to the WORKSPACE file. The rule is tested by adding a proof-of-concept test target to the scala examples.

Link to discussion thread: https://groups.google.com/d/msg/bazel-dev/5Kbbwr11XOA/znTY-H1DBgAJ

Fixes #503.

--
Reviewed-on: https://github.com/bazelbuild/bazel/pull/657
MOS_MIGRATED_REVID=113526680
diff --git a/WORKSPACE b/WORKSPACE
index ebe5700..1cb41a8 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -52,3 +52,11 @@
     url = "http://downloads.typesafe.com/scala/2.11.7/scala-2.11.7.tgz",
     build_file = "tools/build_defs/scala/scala.BUILD",
 )
+
+# only used for the scala test rule
+http_file(
+    name = "scalatest",
+    url = "https://oss.sonatype.org/content/groups/public/org/scalatest/scalatest_2.11/2.2.6/scalatest_2.11-2.2.6.jar",
+    sha256 = "f198967436a5e7a69cfd182902adcfbcb9f2e41b349e1a5c8881a2407f615962",
+)
+
diff --git a/tools/build_defs/scala/README.md b/tools/build_defs/scala/README.md
index c2db304..1bf9980 100644
--- a/tools/build_defs/scala/README.md
+++ b/tools/build_defs/scala/README.md
@@ -5,17 +5,18 @@
   <ul>
     <li><a href="#scala_library">scala_library/scala_macro_library</a></li>
     <li><a href="#scala_binary">scala_binary</a></li>
+    <li><a href="#scala_test">scala_test</a></li>
   </ul>
 </div>
 
 ## Overview
 
 This rule is used for building [Scala][scala] projects with Bazel. There are
-currently three rules, `scala_library`, `scala_macro_library` and
-`scala_binary`. More features will be added in the future, e.g. `scala_test`.
+currently four rules, `scala_library`, `scala_macro_library`, `scala_binary`
+and `scala_test`.
 
-In order to use this build rule, you must add the following to your WORKSPACE
-file:
+In order to use `scala_library`, `scala_macro_library`, and `scala_binary`,
+you must add the following to your WORKSPACE file:
 ```python
 new_http_archive(
     name = "scala",
@@ -26,6 +27,16 @@
 )
 ```
 
+In addition, in order to use `scala_test`, you must add the following to your
+WORKSPACE file:
+```python
+http_file(
+    name = "scalatest",
+    url = "https://oss.sonatype.org/content/groups/public/org/scalatest/scalatest_2.11/2.2.6/scalatest_2.11-2.2.6.jar",
+    sha256 = "f198967436a5e7a69cfd182902adcfbcb9f2e41b349e1a5c8881a2407f615962",
+)
+```
+
 [scala]: http://www.scala-lang.org/
 
 <a name="scala_library"></a>
@@ -234,3 +245,19 @@
     </tr>
   </tbody>
 </table>
+
+<a name="scala_test"></a>
+## scala_test
+
+```python
+scala_test(name, srcs, suites, deps, data, main_class, resources, scalacopts, jvm_flags)
+```
+
+`scala_test` generates a Scala executable which runs unit test suites written
+using the `scalatest` library. It may depend on `scala_library`,
+`scala_macro_library` and `java_library` rules.
+
+A `scala_test` requires a `suites` attribute, specifying the fully qualified
+(canonical) names of the test suites to run. In a future version, we might
+investigate lifting this requirement.
+
diff --git a/tools/build_defs/scala/scala.bzl b/tools/build_defs/scala/scala.bzl
index 488d526..10ac1ae 100644
--- a/tools/build_defs/scala/scala.bzl
+++ b/tools/build_defs/scala/scala.bzl
@@ -85,9 +85,10 @@
 def _write_launcher(ctx, jars):
   content = """#!/bin/bash
 cd $0.runfiles
-java -cp {cp} {name} "$@"
+{java} -cp {cp} {name} "$@"
 """
   content = content.format(
+      java=ctx.file._java.path,
       name=ctx.attr.main_class,
       deploy_jar=ctx.outputs.jar.path,
       cp=":".join([j.short_path for j in jars]))
@@ -95,6 +96,27 @@
       output=ctx.outputs.executable,
       content=content)
 
+def _args_for_suites(suites):
+  args = ["-o"]
+  for suite in suites:
+    args.extend(["-s", suite])
+  return args
+
+def _write_test_launcher(ctx, jars):
+  content = """#!/bin/bash
+cd $0.runfiles
+{java} -cp {cp} {name} {args} "$@"
+"""
+  content = content.format(
+      java=ctx.file._java.path,
+      name=ctx.attr.main_class,
+      args=' '.join(_args_for_suites(ctx.attr.suites)),
+      deploy_jar=ctx.outputs.jar.path,
+      cp=":".join([j.short_path for j in jars]))
+  ctx.file_action(
+      output=ctx.outputs.executable,
+      content=content)
+
 def _collect_comp_run_jars(ctx):
   compile_jars = set()
   runtime_jars = set()
@@ -140,43 +162,60 @@
       interface_jar_files=cjars,
       runfiles=runfiles)
 
-def _scala_binary_impl(ctx):
-  (cjars, rjars) = _collect_comp_run_jars(ctx)
+# Common code shared by all scala binary implementations.
+def _scala_binary_common(ctx, cjars, rjars):
   _write_manifest(ctx)
   _compile(ctx, cjars, False)
 
-  rjars += [ctx.outputs.jar, ctx.file._scalalib]
-  _write_launcher(ctx, rjars)
-
   runfiles = ctx.runfiles(
-      files = list(rjars) + [ctx.outputs.executable],
+      files = list(rjars) + [ctx.outputs.executable] + [ctx.file._java] + ctx.files._jdk,
       collect_data = True)
   return struct(
       files=set([ctx.outputs.executable]),
       runfiles=runfiles)
 
+def _scala_binary_impl(ctx):
+  (cjars, rjars) = _collect_comp_run_jars(ctx)
+  cjars += [ctx.file._scalareflect]
+  rjars += [ctx.outputs.jar, ctx.file._scalalib, ctx.file._scalareflect]
+  _write_launcher(ctx, rjars)
+  return _scala_binary_common(ctx, cjars, rjars)
+
+def _scala_test_impl(ctx):
+  (cjars, rjars) = _collect_comp_run_jars(ctx)
+  cjars += [ctx.file._scalareflect, ctx.file._scalatest, ctx.file._scalaxml]
+  rjars += [ctx.outputs.jar, ctx.file._scalalib, ctx.file._scalareflect, ctx.file._scalatest, ctx.file._scalaxml]
+  _write_test_launcher(ctx, rjars)
+  return _scala_binary_common(ctx, cjars, rjars)
+
 _implicit_deps = {
   "_ijar": attr.label(executable=True, default=Label("//tools/defaults:ijar"), single_file=True, allow_files=True),
   "_scalac": attr.label(executable=True, default=Label("@scala//:bin/scalac"), single_file=True, allow_files=True),
   "_scalalib": attr.label(default=Label("@scala//:lib/scala-library.jar"), single_file=True, allow_files=True),
+  "_scalaxml": attr.label(default=Label("@scala//:lib/scala-xml_2.11-1.0.4.jar"), single_file=True, allow_files=True),
   "_scalasdk": attr.label(default=Label("@scala//:sdk"), allow_files=True),
+  "_scalareflect": attr.label(default=Label("@scala//:lib/scala-reflect.jar"), single_file=True, allow_files=True),
   "_jar": attr.label(executable=True, default=Label("@bazel_tools//tools/jdk:jar"), single_file=True, allow_files=True),
   "_jdk": attr.label(default=Label("//tools/defaults:jdk"), allow_files=True),
 }
 
+# Common attributes reused across multiple rules.
+_common_attrs = {
+  "srcs": attr.label_list(
+      allow_files=_scala_filetype,
+      non_empty=True),
+  "deps": attr.label_list(),
+  "data": attr.label_list(allow_files=True, cfg=DATA_CFG),
+  "resources": attr.label_list(allow_files=True),
+  "scalacopts":attr.string_list(),
+  "jvm_flags": attr.string_list(),
+}
+
 scala_library = rule(
   implementation=_scala_library_impl,
   attrs={
       "main_class": attr.string(),
-      "srcs": attr.label_list(
-          allow_files=_scala_filetype,
-          non_empty=True),
-      "deps": attr.label_list(),
-      "data": attr.label_list(allow_files=True, cfg=DATA_CFG),
-      "resources": attr.label_list(allow_files=True),
-      "scalacopts": attr.string_list(),
-      "jvm_flags": attr.string_list(),
-      } + _implicit_deps,
+      } + _implicit_deps + _common_attrs,
   outputs={
       "jar": "%{name}_deploy.jar",
       "ijar": "%{name}_ijar.jar",
@@ -188,16 +227,8 @@
   implementation=_scala_macro_library_impl,
   attrs={
       "main_class": attr.string(),
-      "srcs": attr.label_list(
-          allow_files=_scala_filetype,
-          non_empty=True),
-      "deps": attr.label_list(),
-      "data": attr.label_list(allow_files=True, cfg=DATA_CFG),
-      "resources": attr.label_list(allow_files=True),
-      "scalacopts": attr.string_list(),
-      "jvm_flags": attr.string_list(),
       "_scala-reflect": attr.label(default=Label("@scala//:lib/scala-reflect.jar"), single_file=True, allow_files=True),
-      } + _implicit_deps,
+      } + _implicit_deps + _common_attrs,
   outputs={
       "jar": "%{name}_deploy.jar",
       "manifest": "%{name}_MANIFEST.MF",
@@ -208,18 +239,27 @@
   implementation=_scala_binary_impl,
   attrs={
       "main_class": attr.string(mandatory=True),
-      "srcs": attr.label_list(
-          allow_files=_scala_filetype,
-          non_empty=True),
-      "deps": attr.label_list(),
-      "data": attr.label_list(allow_files=True, cfg=DATA_CFG),
-      "resources": attr.label_list(allow_files=True),
-      "scalacopts":attr.string_list(),
-      "jvm_flags": attr.string_list(),
-      } + _implicit_deps,
+      "_java": attr.label(executable=True, default=Label("@bazel_tools//tools/jdk:java"), single_file=True, allow_files=True),
+      } + _implicit_deps + _common_attrs,
   outputs={
       "jar": "%{name}_deploy.jar",
       "manifest": "%{name}_MANIFEST.MF",
       },
   executable=True,
 )
+
+scala_test = rule(
+  implementation=_scala_test_impl,
+  attrs={
+      "main_class": attr.string(default="org.scalatest.tools.Runner"),
+      "suites": attr.string_list(),
+      "_scalatest": attr.label(executable=True, default=Label("@scalatest//file"), single_file=True, allow_files=True),
+      "_java": attr.label(executable=True, default=Label("@bazel_tools//tools/jdk:java"), single_file=True, allow_files=True),
+      } + _implicit_deps + _common_attrs,
+  outputs={
+      "jar": "%{name}_deploy.jar",
+      "manifest": "%{name}_MANIFEST.MF",
+      },
+  executable=True,
+  test=True,
+)
diff --git a/tools/build_defs/scala/test/BUILD b/tools/build_defs/scala/test/BUILD
index 4000e0c..6f77bb4 100644
--- a/tools/build_defs/scala/test/BUILD
+++ b/tools/build_defs/scala/test/BUILD
@@ -33,6 +33,19 @@
     ],
 )
 
+scala_test(
+    name = "HelloLibTest",
+    size = "medium",  # Not a macro, can pass test-specific attributes.
+    srcs = ["HelloLibTest.scala"],
+    suites = [
+        "scala.test.ScalaSuite",
+        "scala.test.JavaSuite",
+    ],
+    deps = [
+        ":HelloLib",
+    ],
+)
+
 scala_library(
     name = "OtherLib",
     srcs = ["OtherLib.scala"],
diff --git a/tools/build_defs/scala/test/HelloLib.scala b/tools/build_defs/scala/test/HelloLib.scala
index 88db454..a1fef11 100644
--- a/tools/build_defs/scala/test/HelloLib.scala
+++ b/tools/build_defs/scala/test/HelloLib.scala
@@ -2,7 +2,15 @@
 
 object HelloLib {
   def printMessage(arg: String) {
-    println(arg + " " + OtherLib.getMessage())
-    println(arg + " " + OtherJavaLib.getMessage())
+    println(getOtherLibMessage(arg))
+    println(getOtherJavaLibMessage(arg))
+  }
+
+  def getOtherLibMessage(arg: String) : String = {
+    arg + " " + OtherLib.getMessage()
+  }
+
+  def getOtherJavaLibMessage(arg: String) : String = {
+    arg + " " + OtherJavaLib.getMessage()
   }
 }
diff --git a/tools/build_defs/scala/test/HelloLibTest.scala b/tools/build_defs/scala/test/HelloLibTest.scala
new file mode 100644
index 0000000..7f5cb5f
--- /dev/null
+++ b/tools/build_defs/scala/test/HelloLibTest.scala
@@ -0,0 +1,16 @@
+package scala.test
+
+import org.scalatest._
+
+class ScalaSuite extends FlatSpec {
+  "HelloLib" should "call scala" in {
+    assert(HelloLib.getOtherLibMessage("hello").equals("hello scala!"))
+  }
+}
+
+class JavaSuite extends FlatSpec {
+  "HelloLib" should "call java" in {
+    assert(HelloLib.getOtherJavaLibMessage("hello").equals("hello java!"))
+  }
+}
+