Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
7e2e198
adding junit scala test running
ittaiz Jan 8, 2017
34620c8
discovered_classes output actually redirected to file
ittaiz Mar 6, 2017
41c0a48
added discovered_classes to rjars and so removed cp hack and reusing …
ittaiz Mar 6, 2017
60caa69
explicitly adding the discovered_classes file to the runfiles
ittaiz Mar 7, 2017
410018d
removed stale comment
ittaiz Mar 7, 2017
287921f
changed target name so it's clear target name is not bound to class name
ittaiz Mar 7, 2017
6fd76c9
explained a todo better, removed allow_files since it's included in s…
ittaiz Mar 7, 2017
87dbf01
split whitespace and metadata filtering
ittaiz Mar 7, 2017
ea10b40
added comment about the usage of grep in skylark
ittaiz Mar 7, 2017
9effdb9
added support for runtime deps and compile time deps
ittaiz Mar 7, 2017
f9f084c
added support for jvm_flags and multiple custom suffixes
ittaiz Mar 8, 2017
3158e76
support patterns and not only suffixes
ittaiz Mar 8, 2017
0033f21
added specs2 junit support
ittaiz Mar 8, 2017
d0e7089
extracted specs2 version to method
ittaiz Mar 9, 2017
5c57a83
removed comments
ittaiz Mar 9, 2017
5161db1
removed another comment
ittaiz Mar 9, 2017
99a639c
moved to depend on an isolated dependency
ittaiz Mar 9, 2017
eb7095f
apparently -a doesn't add untracked
ittaiz Mar 9, 2017
30fef56
moved from unzip to jar and reformatted DiscoveredTestSuite
ittaiz Mar 14, 2017
64a6404
added test to show junit failures are supported
ittaiz Mar 14, 2017
97583a1
added a test to show xml files are generated
ittaiz Mar 21, 2017
558640d
moved from filegroup in scala.bzl to src/scala ones
ittaiz Mar 21, 2017
d52045a
removed scalac warning
ittaiz Mar 21, 2017
c228907
added comment to clarify design
ittaiz Mar 21, 2017
0f6cd99
moved to using bazel jar
ittaiz Mar 21, 2017
9dd6e56
Trigger
ittaiz Mar 22, 2017
3524c4e
fixed formatting comments
ittaiz Mar 24, 2017
5572178
Revert "moved from filegroup in scala.bzl to src/scala ones"
ittaiz Mar 24, 2017
66f5eb3
moved from explicitly depending on @scala to using bind
ittaiz Mar 24, 2017
b8faa37
moved from patterns to prefixes/suffixes
ittaiz Mar 26, 2017
4cc722d
removed unneeded globs
ittaiz Mar 26, 2017
853679a
removed unused imports
ittaiz Mar 26, 2017
bb23f05
renamed CustomPattern to CustomPrefix
ittaiz Mar 26, 2017
0325608
mandating at least one of prefixes/suffixes attributes be set
ittaiz Mar 26, 2017
fe20920
erroring when no tests are discovered
ittaiz Mar 26, 2017
22ccd22
multiple prefixes test, dropped single suffix and single prefix tests…
ittaiz Mar 26, 2017
7d1a915
Merge branch 'master' into junit_support
ittaiz Mar 27, 2017
845fb2f
Fixed typo
ittaiz Mar 27, 2017
39824a7
another typo from the script
ittaiz Mar 28, 2017
d71bc1d
Merge remote-tracking branch 'origin/master' into junit_support
ittaiz Mar 28, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions WORKSPACE
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ tut_repositories()
load("//jmh:jmh.bzl", "jmh_repositories")
jmh_repositories()

load("//specs2:specs2_junit.bzl","specs2_junit_repositories")
specs2_junit_repositories()

# test adding a scala jar:
maven_jar(
name = "com_twitter__scalding_date",
Expand Down
Empty file added junit/BUILD
Empty file.
14 changes: 14 additions & 0 deletions junit/junit.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
def junit_repositories():
native.maven_jar(
name = "io_bazel_rules_scala_junit_junit",
artifact = "junit:junit:4.12",
sha1 = "2973d150c0dc1fefe998f834810d68f278ea58ec",
)
native.bind(name = 'io_bazel_rules_scala/dependency/junit/junit', actual = '@io_bazel_rules_scala_junit_junit//jar')

native.maven_jar(
name = "io_bazel_rules_scala_org_hamcrest_hamcrest_core",
artifact = "org.hamcrest:hamcrest-core:1.3",
sha1 = "42a25dc3219429f0e5d060061f71acb49bf010a0",
)
native.bind(name = 'io_bazel_rules_scala/dependency/hamcrest/hamcrest_core', actual = '@io_bazel_rules_scala_org_hamcrest_hamcrest_core//jar')
92 changes: 90 additions & 2 deletions scala/scala.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

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

load("//specs2:specs2_junit.bzl", "specs2_junit_dependencies")
_jar_filetype = FileType([".jar"])
_java_filetype = FileType([".java"])
_scala_filetype = FileType([".scala"])
Expand Down Expand Up @@ -535,8 +536,7 @@ def _scala_test_impl(ctx):
jars = _collect_jars(deps)
(cjars, rjars) = (jars.compiletime, jars.runtime)
cjars += [ctx.file._scalareflect, ctx.file._scalatest, ctx.file._scalaxml]
rjars += [
ctx.outputs.jar,
rjars += [ctx.outputs.jar,
ctx.file._scalalib,
ctx.file._scalareflect,
ctx.file._scalatest,
Expand All @@ -561,6 +561,40 @@ def _scala_test_impl(ctx):
)
return _scala_binary_common(ctx, cjars, rjars)

def _gen_test_suite_flags_based_on_prefixes_and_suffixes(ctx, archive):
return struct(suite_class = "io.bazel.rulesscala.test_discovery.DiscoveredTestSuite",
archiveFlag = "-Dbazel.discover.classes.archive.file.path=%s" % archive.short_path,
prefixesFlag = "-Dbazel.discover.classes.prefixes=%s" % ",".join(ctx.attr.prefixes),
suffixesFlag = "-Dbazel.discover.classes.suffixes=%s" % ",".join(ctx.attr.suffixes),
printFlag = "-Dbazel.discover.classes.print.discovered=%s" % ctx.attr.print_discovered_classes)

def _scala_junit_test_impl(ctx):
if (not(ctx.attr.prefixes) and not(ctx.attr.suffixes)):
fail("Setting at least one of the attributes ('prefixes','suffixes') is required")
deps = ctx.attr.deps + [ctx.attr._suite]
jars = _collect_jars(deps)
(cjars, rjars) = (jars.compiletime, jars.runtime)
junit_deps = [ctx.file._junit,ctx.file._hamcrest]
cjars += junit_deps
rjars += [ctx.outputs.jar,
ctx.file._scalalib
] + junit_deps
rjars += _collect_jars(ctx.attr.runtime_deps).runtime
test_suite = _gen_test_suite_flags_based_on_prefixes_and_suffixes(ctx, ctx.outputs.jar)
launcherJvmFlags = ["-ea", test_suite.archiveFlag, test_suite.prefixesFlag, test_suite.suffixesFlag, test_suite.printFlag]
_write_launcher(
ctx = ctx,
rjars = rjars,
main_class = "org.junit.runner.JUnitCore",
jvm_flags = launcherJvmFlags + ctx.attr.jvm_flags,
args = test_suite.suite_class,
run_before_binary = "",
run_after_binary = "",
)

return _scala_binary_common(ctx, cjars, rjars)


_implicit_deps = {
"_ijar": attr.label(executable=True, cfg="host", default=Label("@bazel_tools//tools/jdk:ijar"), allow_files=True),
"_scalac": attr.label(executable=True, cfg="host", default=Label("//src/java/io/bazel/rulesscala/scalac"), allow_files=True),
Expand Down Expand Up @@ -687,6 +721,30 @@ exports_files([
"lib/scala-xml_2.11-1.0.4.jar",
"lib/scalap-2.11.8.jar",
])

filegroup(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we already have these here:

https://github.com/bazelbuild/rules_scala/blob/master/src/scala/BUILD#L1

Can we consolidate on those targets? Why were these needed?

I'm concerned about supporting 2.12 and 2.11 with all these direct references in different places.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can, they were needed since using "@scala//:lib/scala-reflect.jar" as a label didn't work for me. If you know how I can skip that I'd love to know. I know my PR notes were long but this is one of the things I noted there.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:( Out of luck.

____Loading complete. Analyzing... ERROR: /Users/ittaiz/workspace/rules_scala/test/BUILD:258:1: no such target '//src/scala:parser_xml': target 'parser_xml' not declared in package 'src/scala' defined by /Users/ittaiz/workspace/rules_scala/src/scala/BUILD and referenced by '//test:Specs2Tests'. ERROR: /Users/ittaiz/workspace/rules_scala/test/BUILD:258:1: no such target '//src/scala:parser_library': target 'parser_library' not declared in package 'src/scala' defined by /Users/ittaiz/workspace/rules_scala/src/scala/BUILD and referenced by '//test:Specs2Tests'. ERROR: /Users/ittaiz/workspace/rules_scala/test/BUILD:258:1: no such target '//src/scala:parser_reflect': target 'parser_reflect' not declared in package 'src/scala' defined by /Users/ittaiz/workspace/rules_scala/src/scala/BUILD and referenced by '//test:Specs2Tests'. ERROR: Analysis of target '//test:Specs2Tests' failed; build aborted. ____Elapsed time: 0.073s

Any idea why I'm getting this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't the target //src/scala:scala_xml? It seems you have :parser_xml.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any comment about this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, moved to //src/scala.
Gave @scala//:lib/scala-library.jar another try but got the following message so am staying with the filegroup.

ERROR: /Users/ittaiz/workspace/rules_scala/test/BUILD:258:1: in deps attribute of scala_junit_test rule //test:Specs2Tests: file '@scala//:lib/scala-library.jar' is misplaced here (expected no files). Since this rule was created by the macro 'scala_specs2_junit_test', the error might have been caused by the macro implementation in /Users/ittaiz/workspace/rules_scala/scala/scala.bzl:817:11.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@johnynek //src/scala doesn't work when I try to use it in an external repository.
I think it's because it's a macro.
If I prefix it with the name of the rules_scala repo in my workspace then it works but that doesn't sound like the right solution since people might name it otherwise, right?
Any thoughts?

name = "scala-xml",
srcs = ["lib/scala-xml_2.11-1.0.4.jar"],
visibility = ["//visibility:public"],
)

filegroup(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are these needed at all rather than @scala//lib/scala-parser-combinators_2.11-1.0.4.jar? Are you just trying to avoid putting the version number (which I think is a great goal, but probably better focused on a PR that can tackle abstracting the scala dependencies so we can handle more than one scala version easily).

name = "scala-parser-combinators",
srcs = ["lib/scala-parser-combinators_2.11-1.0.4.jar"],
visibility = ["//visibility:public"],
)

filegroup(
name = "scala-library",
srcs = ["lib/scala-library.jar"],
visibility = ["//visibility:public"],
)

filegroup(
name = "scala-reflect",
srcs = ["lib/scala-reflect.jar"],
visibility = ["//visibility:public"],
)
"""

def scala_repositories():
Expand Down Expand Up @@ -722,6 +780,14 @@ def scala_repositories():
server = "scalac_deps_maven_server",
)

native.bind(name = 'io_bazel_rules_scala/dependency/scala/scala_xml', actual = '@scala//:scala-xml')

native.bind(name = 'io_bazel_rules_scala/dependency/scala/parser_combinators', actual = '@scala//:scala-parser-combinators')

native.bind(name = 'io_bazel_rules_scala/dependency/scala/scala_library', actual = '@scala//:scala-library')

native.bind(name = 'io_bazel_rules_scala/dependency/scala/scala_reflect', actual = '@scala//:scala-reflect')

def scala_export_to_java(name, exports, runtime_deps):
jars = []
for target in exports:
Expand Down Expand Up @@ -792,4 +858,26 @@ def scala_library_suite(name,
ts.append(n)
scala_library(name = name, deps = ts, exports = exports + ts, visibility = visibility)

scala_junit_test = rule(
implementation=_scala_junit_test_impl,
attrs= _implicit_deps + _common_attrs + {
"prefixes": attr.string_list(default=[]),
"suffixes": attr.string_list(default=[]),
"print_discovered_classes": attr.bool(default=False, mandatory=False),
"_junit": attr.label(default=Label("//external:io_bazel_rules_scala/dependency/junit/junit"), single_file=True),
"_hamcrest": attr.label(default=Label("//external:io_bazel_rules_scala/dependency/hamcrest/hamcrest_core"), single_file=True),
"_suite": attr.label(default=Label("//src/java/io/bazel/rulesscala/test_discovery:test_discovery")),
},
outputs={
"jar": "%{name}.jar",
"deploy_jar": "%{name}_deploy.jar",
"manifest": "%{name}_MANIFEST.MF",
},
test=True,
)

def scala_specs2_junit_test(name, **kwargs):
scala_junit_test(
name = name,
deps = specs2_junit_dependencies() + kwargs.pop("deps",[]),
**kwargs)
Empty file added specs2/BUILD
Empty file.
50 changes: 50 additions & 0 deletions specs2/specs2.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
def specs2_version():
return "3.8.8"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need this special function?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

answered in the other comment about it

def specs2_repositories():

native.maven_jar(
name = "io_bazel_rules_scala_org_specs2_specs2_core",
artifact = "org.specs2:specs2-core_2.11:" + specs2_version(),
sha1 = "495bed00c73483f4f5f43945fde63c615d03e637",
)
native.bind(name = 'io_bazel_rules_scala/dependency/specs2/specs2_core', actual = '@io_bazel_rules_scala_org_specs2_specs2_core//jar')

native.maven_jar(
name = "io_bazel_rules_scala_org_specs2_specs2_common",
artifact = "org.specs2:specs2-common_2.11:" + specs2_version(),
sha1 = "15bc009eaae3a574796c0f558d8696b57ae903c3",
)
native.bind(name = 'io_bazel_rules_scala/dependency/specs2/specs2_common', actual = '@io_bazel_rules_scala_org_specs2_specs2_common//jar')

native.maven_jar(
name = "io_bazel_rules_scala_org_specs2_specs2_matcher",
artifact = "org.specs2:specs2-matcher_2.11:" + specs2_version(),
sha1 = "d2e967737abef7421e47b8994a8c92784e624d62",
)
native.bind(name = 'io_bazel_rules_scala/dependency/specs2/specs2_matcher', actual = '@io_bazel_rules_scala_org_specs2_specs2_matcher//jar')

native.maven_jar(
name = "io_bazel_rules_scala_org_scalaz_scalaz_effect",
artifact = "org.scalaz:scalaz-effect_2.11:7.2.7",
sha1 = "824bbb83da12224b3537c354c51eb3da72c435b5",
)
native.bind(name = 'io_bazel_rules_scala/dependency/scalaz/scalaz_effect', actual = '@io_bazel_rules_scala_org_scalaz_scalaz_effect//jar')

native.maven_jar(
name = "io_bazel_rules_scala_org_scalaz_scalaz_core",
artifact = "org.scalaz:scalaz-core_2.11:7.2.7",
sha1 = "ebf85118d0bf4ce18acebf1d8475ee7deb7f19f1",
)
native.bind(name = 'io_bazel_rules_scala/dependency/scalaz/scalaz_core', actual = '@io_bazel_rules_scala_org_scalaz_scalaz_core//jar')

def specs2_dependencies():
return [
"//external:io_bazel_rules_scala/dependency/specs2/specs2_core",
"//external:io_bazel_rules_scala/dependency/specs2/specs2_common",
"//external:io_bazel_rules_scala/dependency/specs2/specs2_matcher",
"//external:io_bazel_rules_scala/dependency/scalaz/scalaz_effect",
"//external:io_bazel_rules_scala/dependency/scalaz/scalaz_core",
"//external:io_bazel_rules_scala/dependency/scala/scala_xml",
"//external:io_bazel_rules_scala/dependency/scala/parser_combinators",
"//external:io_bazel_rules_scala/dependency/scala/scala_library",
"//external:io_bazel_rules_scala/dependency/scala/scala_reflect"]
16 changes: 16 additions & 0 deletions specs2/specs2_junit.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
load("//specs2:specs2.bzl", "specs2_repositories", "specs2_dependencies", "specs2_version")
load("//junit:junit.bzl", "junit_repositories")

def specs2_junit_repositories():
specs2_repositories()
junit_repositories()
# Aditional dependencies for specs2 junit runner
native.maven_jar(
name = "io_bazel_rules_scala_org_specs2_specs2_junit_2_11",
artifact = "org.specs2:specs2-junit_2.11:" + specs2_version(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see how that function helps (specs2_version) since only one version will have the right sha1 below, no (unless someone generates a sha1 collision for specs2! ;)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I mentioned in the PR notes the reason I added this was because I (mistakenly) used one version in the specs2.bzl file and a different one in the specs2_junit.bzl file. Finding that bug took me a few hours since the runtime error messages are really unclear. Once I found it out I realized my problem was code duplication so I solved it with a method.

sha1 = "1dc9e43970557c308ee313842d84094bc6c1c1b5",
)
native.bind(name = 'io_bazel_rules_scala/dependency/specs2/specs2_junit', actual = '@io_bazel_rules_scala_org_specs2_specs2_junit_2_11//jar')

def specs2_junit_dependencies():
return specs2_dependencies() + ["//external:io_bazel_rules_scala/dependency/specs2/specs2_junit"]
8 changes: 8 additions & 0 deletions src/java/io/bazel/rulesscala/test_discovery/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
load("//scala:scala.bzl", "scala_library")


scala_library(name = "test_discovery",
srcs = ["DiscoveredTestSuite.scala"],
deps = ["//external:io_bazel_rules_scala/dependency/junit/junit"],
visibility = ["//visibility:public"],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package io.bazel.rulesscala.test_discovery

import org.junit.runner.RunWith
import org.junit.runners.Suite
import org.junit.runners.model.RunnerBuilder
import java.io.File
import java.io.FileInputStream
import java.util.jar.JarInputStream
import java.util.jar.JarEntry
/*
The test running and discovery mechanism works in the following manner:
- Bazel rule executes a JVM application to run tests (currently `JUnitCore`) and asks it to run
the `DiscoveredTestSuite` suite.
- When JUnit tries to run it, it uses the `PrefixSuffixTestDiscoveringSuite` runner
to know what tests exist in the suite.
- We know which tests to run by examining the entries of the target's archive.
- The archive's path is passed in a system property ("bazel.discover.classes.archive.file.path").
- The entries of the archive are filtered to keep only classes
- Of those we filter again and keep only those which match either of the prefixes/suffixes supplied.
- Prefixes are supplied as a comma separated list. System property ("bazel.discover.classes.prefixes")
- Suffixes are supplied as a comma separated list. System property ("bazel.discover.classes.prefixes")
- We iterate over the remaining entries and format them into classes.
- At this point we tell JUnit (via the `RunnerBuilder`) what are the discovered test classes.
- W.R.T. discovery semantics this is similar to how maven surefire/failsafe plugins work.
- For debugging purposes one can ask to print the list of discovered classes.
- This is done via an `print_discovered_classes` attribute.
- The attribute is sent via "bazel.discover.classes.print.discovered"

Additional references:
- http://junit.org/junit4/javadoc/4.12/org/junit/runner/RunWith.html
- http://junit.org/junit4/javadoc/4.12/org/junit/runners/model/RunnerBuilder.html
- http://maven.apache.org/surefire/maven-surefire-plugin/examples/inclusion-exclusion.html
*/
@RunWith(classOf[PrefixSuffixTestDiscoveringSuite])
class DiscoveredTestSuite

class PrefixSuffixTestDiscoveringSuite(testClass: Class[Any], builder: RunnerBuilder)
extends Suite(builder, testClass, PrefixSuffixTestDiscoveringSuite.discoverClasses())

object PrefixSuffixTestDiscoveringSuite {

private def discoverClasses(): Array[Class[_]] = {

val archive = archiveInputStream()
val classes = discoverClasses(archive, prefixes, suffixesWithClassSuffix)
archive.close()
if (printDiscoveredClasses) {
println("Discovered classes:")
classes.foreach(c => println(c.getName))
}
if (classes.isEmpty)
throw new IllegalStateException("Was not able to discover any classes " +
s"for archive=$archivePath, " +
s"prefixes=$prefixes, " +
s"suffixes=$suffixes")
classes
}

private def discoverClasses(archive: JarInputStream,
prefixes: Set[String],
suffixes: Set[String]): Array[Class[_]] =
matchingEntries(archive, prefixes, suffixes)
.map(dropFileSuffix)
.map(fileToClassFormat)
.map(Class.forName)
.toArray

private def matchingEntries(archive: JarInputStream,
prefixes: Set[String],
suffixes: Set[String]) =
entries(archive)
.filter(isClass)
.filter(entry => endsWith(suffixes)(entry) || startsWith(prefixes)(entry))

private def startsWith(prefixes: Set[String])(entry: String): Boolean = {
val entryName = entryFileName(entry)
prefixes.exists(entryName.startsWith)
}

private def endsWith(suffixes: Set[String])(entry: String): Boolean = {
val entryName = entryFileName(entry)
suffixes.exists(entryName.endsWith)
}

private def entryFileName(entry: String): String =
new File(entry).getName

private def dropFileSuffix(classEntry: String): String =
classEntry.split("\\.").head

private def fileToClassFormat(classEntry: String): String =
classEntry.replace('/', '.')

private def isClass(entry: String): Boolean =
entry.endsWith(".class")

private def entries(jarInputStream: JarInputStream) =
Stream.continually(Option(jarInputStream.getNextJarEntry))
.takeWhile(_.isDefined)
.flatten
.map(_.getName)
.toList

private def archiveInputStream() =
new JarInputStream(new FileInputStream(archivePath))

private def archivePath: String =
System.getProperty("bazel.discover.classes.archive.file.path")

private def suffixesWithClassSuffix: Set[String] =
suffixes.map(_ + ".class")

private def suffixes: Set[String] =
parseProperty(System.getProperty("bazel.discover.classes.suffixes"))

private def prefixes: Set[String] =
parseProperty(System.getProperty("bazel.discover.classes.prefixes"))

private def parseProperty(potentiallyEmpty: String): Set[String] =
potentiallyEmpty.trim match {
case emptyStr if emptyStr.isEmpty => Set[String]()
case nonEmptyStr => nonEmptyStr.split(",").toSet
}

private def printDiscoveredClasses: Boolean =
System.getProperty("bazel.discover.classes.print.discovered").toBoolean

}
Loading