From 031e9463daf299d62e8be216bab0471fc28d7c76 Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Wed, 3 Apr 2019 09:48:43 -0700 Subject: [PATCH] Improve error reporting for classloading issues We noticed that the community build was failing for some projects due to some class loading issues. My initial approach for detecting the errors didn't always work because the test framework might wrap the underlying exception. To fix that, I add the causes to the list of throwables to scan for class loading related exceptions. I also added ClassNotFoundException to the list of types to check for. I additionally added more context to the error message so that it is more clear to the user what specifically went wrong. The error message is intended to provide examples that the user can actually paste into the console. There is also a lot of manual line wrapping that could be improved by defining paragraphs and then splitting on the jline terminal width. That could be a useful internal helper function to improve our log messages in general. The underlying issue could be addressed by allowing the user to specify libraries that get excluded from the dependency classpath for layering purposes. I'm not sure the best way to do that yet and adding that feature wouldn't fix any existing builds so I think that would be better handled in 1.4.0. --- main/src/main/scala/sbt/Defaults.scala | 45 ++++++++++++++++++++------ 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index b504dd5c0..cfdddd6a7 100755 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -780,7 +780,8 @@ object Defaults extends BuildCommon { (fullClasspath in test).value, testForkedParallel.value, (javaOptions in test).value, - (classLoaderLayeringStrategy).value + (classLoaderLayeringStrategy).value, + projectId = s"${thisProject.value.id} / ", ) } ).value, @@ -952,7 +953,8 @@ object Defaults extends BuildCommon { fullClasspath.value, testForkedParallel.value, javaOptions.value, - classLoaderLayeringStrategy.value + classLoaderLayeringStrategy.value, + projectId = s"${thisProject.value.id} / ", ) val taskName = display.show(resolvedScoped.value) val trl = testResultLogger.value @@ -997,6 +999,7 @@ object Defaults extends BuildCommon { forkedParallelExecution = false, javaOptions = Nil, strategy = ClassLoaderLayeringStrategy.TestDependencies, + projectId = "", ) } @@ -1019,6 +1022,7 @@ object Defaults extends BuildCommon { forkedParallelExecution, javaOptions = Nil, strategy = ClassLoaderLayeringStrategy.TestDependencies, + projectId = "", ) } @@ -1032,6 +1036,7 @@ object Defaults extends BuildCommon { forkedParallelExecution: Boolean, javaOptions: Seq[String], strategy: ClassLoaderLayeringStrategy, + projectId: String ): Initialize[Task[Tests.Output]] = { val runners = createTestRunners(frameworks, loader, config) val groupTasks = groups map { @@ -1061,17 +1066,39 @@ object Defaults extends BuildCommon { val result = output map { out => out.events.foreach { case (suite, e) => - if (strategy != ClassLoaderLayeringStrategy.Flat) { - e.throwables + if (strategy != ClassLoaderLayeringStrategy.Flat || + strategy != ClassLoaderLayeringStrategy.ScalaInstance) { + (e.throwables ++ e.throwables.flatMap(t => Option(t.getCause))) .find { t => - t.isInstanceOf[NoClassDefFoundError] || t.isInstanceOf[IllegalAccessError] + t.isInstanceOf[NoClassDefFoundError] || + t.isInstanceOf[IllegalAccessError] || + t.isInstanceOf[ClassNotFoundException] } .foreach { t => s.log.error( - s"Test suite $suite failed with $t. This may be due to the ClassLoaderLayeringStrategy" - + s" ($strategy) used by your task. This issue may be resolved by changing the" - + " ClassLoaderLayeringStrategy in your configuration (generally Test or IntegrationTest)," - + " e.g.:\nTest / classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.Flat\n" + s"Test suite $suite failed with $t.\nThis may be due to the " + + s"ClassLoaderLayeringStrategy ($strategy) used by your task.\n" + + "To improve performance and reduce memory, sbt attempts to cache the" + + " class loaders used to load the project dependencies.\n" + + "The project class files are loaded in a separate class loader that is" + + " created for each test run.\nThe test class loader accesses the project" + + " dependency classes using the cached project dependency classloader.\nWith" + + " this approach, class loading may fail under the following conditions:\n\n" + + " * Dependencies use reflection to access classes in your project's" + + " classpath.\n Java serialization/deserialization may cause this.\n" + + " * An open package is accessed across layers. If the project's classes" + + " access or extend\n jvm package private classes defined in a" + + " project dependency, it may cause an IllegalAccessError\n because the" + + " jvm enforces package private at the classloader level.\n\n" + + "These issues, along with others that were not enumerated above, may be" + + " resolved by changing the class loader layering strategy.\n" + + "The Flat and ScalaInstance strategies bundle the full project classpath in" + + " the same class loader.\nTo use one of these strategies, set the " + + " ClassLoaderLayeringStrategy key\nin your configuration, for example:\n\n" + + s"set ${projectId}Test / classLoaderLayeringStrategy :=" + + " ClassLoaderLayeringStrategy.ScalaInstance\n" + + s"set ${projectId}Test / classLoaderLayeringStrategy :=" + + " ClassLoaderLayeringStrategy.Flat\n\n" + "See ClassLoaderLayeringStrategy.scala for the full list of options." ) }