Skip to content

Commit b51e54c

Browse files
ittaizjohnynek
authored andcommitted
Junit & Specs2 support (#163)
* adding junit scala test running * discovered_classes output actually redirected to file run files currently manually built DiscoveredTestSuite correctly parses the zip entries JunitTest removes the @RunWith since it's not mandatory * added discovered_classes to rjars and so removed cp hack and reusing shared runfiles * explicitly adding the discovered_classes file to the runfiles * removed stale comment * changed target name so it's clear target name is not bound to class name * explained a todo better, removed allow_files since it's included in single_file) * split whitespace and metadata filtering * added comment about the usage of grep in skylark * added support for runtime deps and compile time deps * added support for jvm_flags and multiple custom suffixes * support patterns and not only suffixes * added specs2 junit support * extracted specs2 version to method * removed comments * removed another comment * moved to depend on an isolated dependency * apparently -a doesn't add untracked * moved from unzip to jar and reformatted DiscoveredTestSuite * added test to show junit failures are supported * added a test to show xml files are generated * moved from filegroup in scala.bzl to src/scala ones * removed scalac warning * added comment to clarify design * moved to using bazel jar * Trigger * fixed formatting comments * Revert "moved from filegroup in scala.bzl to src/scala ones" This reverts commit 558640d. * moved from explicitly depending on @scala to using bind * moved from patterns to prefixes/suffixes moved from using jar/grep to JarInputStream in JVM added print debugging of discovered classes changed e2e tests which check multi files to use print debugging * removed unneeded globs * removed unused imports * renamed CustomPattern to CustomPrefix * mandating at least one of prefixes/suffixes attributes be set * erroring when no tests are discovered * multiple prefixes test, dropped single suffix and single prefix tests since they are covered by the test * Fixed typo * another typo from the script
1 parent 21ebd9c commit b51e54c

File tree

21 files changed

+588
-5
lines changed

21 files changed

+588
-5
lines changed

WORKSPACE

+3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ tut_repositories()
1212
load("//jmh:jmh.bzl", "jmh_repositories")
1313
jmh_repositories()
1414

15+
load("//specs2:specs2_junit.bzl","specs2_junit_repositories")
16+
specs2_junit_repositories()
17+
1518
# test adding a scala jar:
1619
maven_jar(
1720
name = "com_twitter__scalding_date",

junit/BUILD

Whitespace-only changes.

junit/junit.bzl

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
def junit_repositories():
2+
native.maven_jar(
3+
name = "io_bazel_rules_scala_junit_junit",
4+
artifact = "junit:junit:4.12",
5+
sha1 = "2973d150c0dc1fefe998f834810d68f278ea58ec",
6+
)
7+
native.bind(name = 'io_bazel_rules_scala/dependency/junit/junit', actual = '@io_bazel_rules_scala_junit_junit//jar')
8+
9+
native.maven_jar(
10+
name = "io_bazel_rules_scala_org_hamcrest_hamcrest_core",
11+
artifact = "org.hamcrest:hamcrest-core:1.3",
12+
sha1 = "42a25dc3219429f0e5d060061f71acb49bf010a0",
13+
)
14+
native.bind(name = 'io_bazel_rules_scala/dependency/hamcrest/hamcrest_core', actual = '@io_bazel_rules_scala_org_hamcrest_hamcrest_core//jar')

scala/scala.bzl

+90-2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
"""Rules for supporting the Scala language."""
1616

17+
load("//specs2:specs2_junit.bzl", "specs2_junit_dependencies")
1718
_jar_filetype = FileType([".jar"])
1819
_java_filetype = FileType([".java"])
1920
_scala_filetype = FileType([".scala"])
@@ -535,8 +536,7 @@ def _scala_test_impl(ctx):
535536
jars = _collect_jars(deps)
536537
(cjars, rjars) = (jars.compiletime, jars.runtime)
537538
cjars += [ctx.file._scalareflect, ctx.file._scalatest, ctx.file._scalaxml]
538-
rjars += [
539-
ctx.outputs.jar,
539+
rjars += [ctx.outputs.jar,
540540
ctx.file._scalalib,
541541
ctx.file._scalareflect,
542542
ctx.file._scalatest,
@@ -561,6 +561,40 @@ def _scala_test_impl(ctx):
561561
)
562562
return _scala_binary_common(ctx, cjars, rjars)
563563

564+
def _gen_test_suite_flags_based_on_prefixes_and_suffixes(ctx, archive):
565+
return struct(suite_class = "io.bazel.rulesscala.test_discovery.DiscoveredTestSuite",
566+
archiveFlag = "-Dbazel.discover.classes.archive.file.path=%s" % archive.short_path,
567+
prefixesFlag = "-Dbazel.discover.classes.prefixes=%s" % ",".join(ctx.attr.prefixes),
568+
suffixesFlag = "-Dbazel.discover.classes.suffixes=%s" % ",".join(ctx.attr.suffixes),
569+
printFlag = "-Dbazel.discover.classes.print.discovered=%s" % ctx.attr.print_discovered_classes)
570+
571+
def _scala_junit_test_impl(ctx):
572+
if (not(ctx.attr.prefixes) and not(ctx.attr.suffixes)):
573+
fail("Setting at least one of the attributes ('prefixes','suffixes') is required")
574+
deps = ctx.attr.deps + [ctx.attr._suite]
575+
jars = _collect_jars(deps)
576+
(cjars, rjars) = (jars.compiletime, jars.runtime)
577+
junit_deps = [ctx.file._junit,ctx.file._hamcrest]
578+
cjars += junit_deps
579+
rjars += [ctx.outputs.jar,
580+
ctx.file._scalalib
581+
] + junit_deps
582+
rjars += _collect_jars(ctx.attr.runtime_deps).runtime
583+
test_suite = _gen_test_suite_flags_based_on_prefixes_and_suffixes(ctx, ctx.outputs.jar)
584+
launcherJvmFlags = ["-ea", test_suite.archiveFlag, test_suite.prefixesFlag, test_suite.suffixesFlag, test_suite.printFlag]
585+
_write_launcher(
586+
ctx = ctx,
587+
rjars = rjars,
588+
main_class = "org.junit.runner.JUnitCore",
589+
jvm_flags = launcherJvmFlags + ctx.attr.jvm_flags,
590+
args = test_suite.suite_class,
591+
run_before_binary = "",
592+
run_after_binary = "",
593+
)
594+
595+
return _scala_binary_common(ctx, cjars, rjars)
596+
597+
564598
_implicit_deps = {
565599
"_ijar": attr.label(executable=True, cfg="host", default=Label("@bazel_tools//tools/jdk:ijar"), allow_files=True),
566600
"_scalac": attr.label(executable=True, cfg="host", default=Label("//src/java/io/bazel/rulesscala/scalac"), allow_files=True),
@@ -687,6 +721,30 @@ exports_files([
687721
"lib/scala-xml_2.11-1.0.4.jar",
688722
"lib/scalap-2.11.8.jar",
689723
])
724+
725+
filegroup(
726+
name = "scala-xml",
727+
srcs = ["lib/scala-xml_2.11-1.0.4.jar"],
728+
visibility = ["//visibility:public"],
729+
)
730+
731+
filegroup(
732+
name = "scala-parser-combinators",
733+
srcs = ["lib/scala-parser-combinators_2.11-1.0.4.jar"],
734+
visibility = ["//visibility:public"],
735+
)
736+
737+
filegroup(
738+
name = "scala-library",
739+
srcs = ["lib/scala-library.jar"],
740+
visibility = ["//visibility:public"],
741+
)
742+
743+
filegroup(
744+
name = "scala-reflect",
745+
srcs = ["lib/scala-reflect.jar"],
746+
visibility = ["//visibility:public"],
747+
)
690748
"""
691749

692750
def scala_repositories():
@@ -722,6 +780,14 @@ def scala_repositories():
722780
server = "scalac_deps_maven_server",
723781
)
724782

783+
native.bind(name = 'io_bazel_rules_scala/dependency/scala/scala_xml', actual = '@scala//:scala-xml')
784+
785+
native.bind(name = 'io_bazel_rules_scala/dependency/scala/parser_combinators', actual = '@scala//:scala-parser-combinators')
786+
787+
native.bind(name = 'io_bazel_rules_scala/dependency/scala/scala_library', actual = '@scala//:scala-library')
788+
789+
native.bind(name = 'io_bazel_rules_scala/dependency/scala/scala_reflect', actual = '@scala//:scala-reflect')
790+
725791
def scala_export_to_java(name, exports, runtime_deps):
726792
jars = []
727793
for target in exports:
@@ -792,4 +858,26 @@ def scala_library_suite(name,
792858
ts.append(n)
793859
scala_library(name = name, deps = ts, exports = exports + ts, visibility = visibility)
794860

861+
scala_junit_test = rule(
862+
implementation=_scala_junit_test_impl,
863+
attrs= _implicit_deps + _common_attrs + {
864+
"prefixes": attr.string_list(default=[]),
865+
"suffixes": attr.string_list(default=[]),
866+
"print_discovered_classes": attr.bool(default=False, mandatory=False),
867+
"_junit": attr.label(default=Label("//external:io_bazel_rules_scala/dependency/junit/junit"), single_file=True),
868+
"_hamcrest": attr.label(default=Label("//external:io_bazel_rules_scala/dependency/hamcrest/hamcrest_core"), single_file=True),
869+
"_suite": attr.label(default=Label("//src/java/io/bazel/rulesscala/test_discovery:test_discovery")),
870+
},
871+
outputs={
872+
"jar": "%{name}.jar",
873+
"deploy_jar": "%{name}_deploy.jar",
874+
"manifest": "%{name}_MANIFEST.MF",
875+
},
876+
test=True,
877+
)
795878

879+
def scala_specs2_junit_test(name, **kwargs):
880+
scala_junit_test(
881+
name = name,
882+
deps = specs2_junit_dependencies() + kwargs.pop("deps",[]),
883+
**kwargs)

specs2/BUILD

Whitespace-only changes.

specs2/specs2.bzl

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
def specs2_version():
2+
return "3.8.8"
3+
def specs2_repositories():
4+
5+
native.maven_jar(
6+
name = "io_bazel_rules_scala_org_specs2_specs2_core",
7+
artifact = "org.specs2:specs2-core_2.11:" + specs2_version(),
8+
sha1 = "495bed00c73483f4f5f43945fde63c615d03e637",
9+
)
10+
native.bind(name = 'io_bazel_rules_scala/dependency/specs2/specs2_core', actual = '@io_bazel_rules_scala_org_specs2_specs2_core//jar')
11+
12+
native.maven_jar(
13+
name = "io_bazel_rules_scala_org_specs2_specs2_common",
14+
artifact = "org.specs2:specs2-common_2.11:" + specs2_version(),
15+
sha1 = "15bc009eaae3a574796c0f558d8696b57ae903c3",
16+
)
17+
native.bind(name = 'io_bazel_rules_scala/dependency/specs2/specs2_common', actual = '@io_bazel_rules_scala_org_specs2_specs2_common//jar')
18+
19+
native.maven_jar(
20+
name = "io_bazel_rules_scala_org_specs2_specs2_matcher",
21+
artifact = "org.specs2:specs2-matcher_2.11:" + specs2_version(),
22+
sha1 = "d2e967737abef7421e47b8994a8c92784e624d62",
23+
)
24+
native.bind(name = 'io_bazel_rules_scala/dependency/specs2/specs2_matcher', actual = '@io_bazel_rules_scala_org_specs2_specs2_matcher//jar')
25+
26+
native.maven_jar(
27+
name = "io_bazel_rules_scala_org_scalaz_scalaz_effect",
28+
artifact = "org.scalaz:scalaz-effect_2.11:7.2.7",
29+
sha1 = "824bbb83da12224b3537c354c51eb3da72c435b5",
30+
)
31+
native.bind(name = 'io_bazel_rules_scala/dependency/scalaz/scalaz_effect', actual = '@io_bazel_rules_scala_org_scalaz_scalaz_effect//jar')
32+
33+
native.maven_jar(
34+
name = "io_bazel_rules_scala_org_scalaz_scalaz_core",
35+
artifact = "org.scalaz:scalaz-core_2.11:7.2.7",
36+
sha1 = "ebf85118d0bf4ce18acebf1d8475ee7deb7f19f1",
37+
)
38+
native.bind(name = 'io_bazel_rules_scala/dependency/scalaz/scalaz_core', actual = '@io_bazel_rules_scala_org_scalaz_scalaz_core//jar')
39+
40+
def specs2_dependencies():
41+
return [
42+
"//external:io_bazel_rules_scala/dependency/specs2/specs2_core",
43+
"//external:io_bazel_rules_scala/dependency/specs2/specs2_common",
44+
"//external:io_bazel_rules_scala/dependency/specs2/specs2_matcher",
45+
"//external:io_bazel_rules_scala/dependency/scalaz/scalaz_effect",
46+
"//external:io_bazel_rules_scala/dependency/scalaz/scalaz_core",
47+
"//external:io_bazel_rules_scala/dependency/scala/scala_xml",
48+
"//external:io_bazel_rules_scala/dependency/scala/parser_combinators",
49+
"//external:io_bazel_rules_scala/dependency/scala/scala_library",
50+
"//external:io_bazel_rules_scala/dependency/scala/scala_reflect"]

specs2/specs2_junit.bzl

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
load("//specs2:specs2.bzl", "specs2_repositories", "specs2_dependencies", "specs2_version")
2+
load("//junit:junit.bzl", "junit_repositories")
3+
4+
def specs2_junit_repositories():
5+
specs2_repositories()
6+
junit_repositories()
7+
# Aditional dependencies for specs2 junit runner
8+
native.maven_jar(
9+
name = "io_bazel_rules_scala_org_specs2_specs2_junit_2_11",
10+
artifact = "org.specs2:specs2-junit_2.11:" + specs2_version(),
11+
sha1 = "1dc9e43970557c308ee313842d84094bc6c1c1b5",
12+
)
13+
native.bind(name = 'io_bazel_rules_scala/dependency/specs2/specs2_junit', actual = '@io_bazel_rules_scala_org_specs2_specs2_junit_2_11//jar')
14+
15+
def specs2_junit_dependencies():
16+
return specs2_dependencies() + ["//external:io_bazel_rules_scala/dependency/specs2/specs2_junit"]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
load("//scala:scala.bzl", "scala_library")
2+
3+
4+
scala_library(name = "test_discovery",
5+
srcs = ["DiscoveredTestSuite.scala"],
6+
deps = ["//external:io_bazel_rules_scala/dependency/junit/junit"],
7+
visibility = ["//visibility:public"],
8+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package io.bazel.rulesscala.test_discovery
2+
3+
import org.junit.runner.RunWith
4+
import org.junit.runners.Suite
5+
import org.junit.runners.model.RunnerBuilder
6+
import java.io.File
7+
import java.io.FileInputStream
8+
import java.util.jar.JarInputStream
9+
import java.util.jar.JarEntry
10+
/*
11+
The test running and discovery mechanism works in the following manner:
12+
- Bazel rule executes a JVM application to run tests (currently `JUnitCore`) and asks it to run
13+
the `DiscoveredTestSuite` suite.
14+
- When JUnit tries to run it, it uses the `PrefixSuffixTestDiscoveringSuite` runner
15+
to know what tests exist in the suite.
16+
- We know which tests to run by examining the entries of the target's archive.
17+
- The archive's path is passed in a system property ("bazel.discover.classes.archive.file.path").
18+
- The entries of the archive are filtered to keep only classes
19+
- Of those we filter again and keep only those which match either of the prefixes/suffixes supplied.
20+
- Prefixes are supplied as a comma separated list. System property ("bazel.discover.classes.prefixes")
21+
- Suffixes are supplied as a comma separated list. System property ("bazel.discover.classes.prefixes")
22+
- We iterate over the remaining entries and format them into classes.
23+
- At this point we tell JUnit (via the `RunnerBuilder`) what are the discovered test classes.
24+
- W.R.T. discovery semantics this is similar to how maven surefire/failsafe plugins work.
25+
- For debugging purposes one can ask to print the list of discovered classes.
26+
- This is done via an `print_discovered_classes` attribute.
27+
- The attribute is sent via "bazel.discover.classes.print.discovered"
28+
29+
Additional references:
30+
- http://junit.org/junit4/javadoc/4.12/org/junit/runner/RunWith.html
31+
- http://junit.org/junit4/javadoc/4.12/org/junit/runners/model/RunnerBuilder.html
32+
- http://maven.apache.org/surefire/maven-surefire-plugin/examples/inclusion-exclusion.html
33+
*/
34+
@RunWith(classOf[PrefixSuffixTestDiscoveringSuite])
35+
class DiscoveredTestSuite
36+
37+
class PrefixSuffixTestDiscoveringSuite(testClass: Class[Any], builder: RunnerBuilder)
38+
extends Suite(builder, testClass, PrefixSuffixTestDiscoveringSuite.discoverClasses())
39+
40+
object PrefixSuffixTestDiscoveringSuite {
41+
42+
private def discoverClasses(): Array[Class[_]] = {
43+
44+
val archive = archiveInputStream()
45+
val classes = discoverClasses(archive, prefixes, suffixesWithClassSuffix)
46+
archive.close()
47+
if (printDiscoveredClasses) {
48+
println("Discovered classes:")
49+
classes.foreach(c => println(c.getName))
50+
}
51+
if (classes.isEmpty)
52+
throw new IllegalStateException("Was not able to discover any classes " +
53+
s"for archive=$archivePath, " +
54+
s"prefixes=$prefixes, " +
55+
s"suffixes=$suffixes")
56+
classes
57+
}
58+
59+
private def discoverClasses(archive: JarInputStream,
60+
prefixes: Set[String],
61+
suffixes: Set[String]): Array[Class[_]] =
62+
matchingEntries(archive, prefixes, suffixes)
63+
.map(dropFileSuffix)
64+
.map(fileToClassFormat)
65+
.map(Class.forName)
66+
.toArray
67+
68+
private def matchingEntries(archive: JarInputStream,
69+
prefixes: Set[String],
70+
suffixes: Set[String]) =
71+
entries(archive)
72+
.filter(isClass)
73+
.filter(entry => endsWith(suffixes)(entry) || startsWith(prefixes)(entry))
74+
75+
private def startsWith(prefixes: Set[String])(entry: String): Boolean = {
76+
val entryName = entryFileName(entry)
77+
prefixes.exists(entryName.startsWith)
78+
}
79+
80+
private def endsWith(suffixes: Set[String])(entry: String): Boolean = {
81+
val entryName = entryFileName(entry)
82+
suffixes.exists(entryName.endsWith)
83+
}
84+
85+
private def entryFileName(entry: String): String =
86+
new File(entry).getName
87+
88+
private def dropFileSuffix(classEntry: String): String =
89+
classEntry.split("\\.").head
90+
91+
private def fileToClassFormat(classEntry: String): String =
92+
classEntry.replace('/', '.')
93+
94+
private def isClass(entry: String): Boolean =
95+
entry.endsWith(".class")
96+
97+
private def entries(jarInputStream: JarInputStream) =
98+
Stream.continually(Option(jarInputStream.getNextJarEntry))
99+
.takeWhile(_.isDefined)
100+
.flatten
101+
.map(_.getName)
102+
.toList
103+
104+
private def archiveInputStream() =
105+
new JarInputStream(new FileInputStream(archivePath))
106+
107+
private def archivePath: String =
108+
System.getProperty("bazel.discover.classes.archive.file.path")
109+
110+
private def suffixesWithClassSuffix: Set[String] =
111+
suffixes.map(_ + ".class")
112+
113+
private def suffixes: Set[String] =
114+
parseProperty(System.getProperty("bazel.discover.classes.suffixes"))
115+
116+
private def prefixes: Set[String] =
117+
parseProperty(System.getProperty("bazel.discover.classes.prefixes"))
118+
119+
private def parseProperty(potentiallyEmpty: String): Set[String] =
120+
potentiallyEmpty.trim match {
121+
case emptyStr if emptyStr.isEmpty => Set[String]()
122+
case nonEmptyStr => nonEmptyStr.split(",").toSet
123+
}
124+
125+
private def printDiscoveredClasses: Boolean =
126+
System.getProperty("bazel.discover.classes.print.discovered").toBoolean
127+
128+
}

0 commit comments

Comments
 (0)