package local import java.lang.reflect.InvocationTargetException import sbt.* import sbt.internal.inc.ScalaInstance import sbt.internal.inc.classpath.{ ClasspathUtilities, FilteredLoader } import scala.annotation.nowarn import scala.jdk.CollectionConverters.* object LocalScriptedPlugin extends AutoPlugin: override def requires = plugins.JvmPlugin object autoImport extends ScriptedKeys trait ScriptedKeys: val publishLocalBinAll = taskKey[Unit]("") val scriptedUnpublished = inputKey[Unit]( "Execute scripted without publishing sbt first. " + "Saves you some time when only your test has changed" ) val scriptedSource = settingKey[File]("") @transient val scriptedPrescripted = taskKey[File => Unit]("") val scriptedKeepTempDirectory = settingKey[Boolean]( "If true, keeps the temporary directory after scripted tests complete for debugging." ) object Scripted: // This is to workaround https://github.com/sbt/io/issues/110 if !sys.props.contains("jna.nosys") then sys.props.put("jna.nosys", "true") val RepoOverrideTest = config("repoOverrideTest").extend(Compile) val sbtWindowsExcludeFilter: FileFilter = if scala.util.Properties.isWin then new SimpleFileFilter(f => (f.getParentFile.getName, f.getName) match case ("classloader-cache", "jni") => true // no native lib is built for windows case ("classloader-cache", "spark") => true // the test spark server is unable to bind to a local socket on Visual Studio 2019 case ("nio", "make-clone") => true // uses gcc which isn't set up on all systems case ("watch", "symlinks") => true // symlinks don't work the same on windows case ("cache", "compile-io-failure") => true // a read-only dir doesn't block writes on windows, so the I/O failure won't reproduce case _ => false ) else NothingFilter import sbt.complete.* // Paging, 1-index based. final case class ScriptedTestPage(page: Int, total: Int) // FIXME: Duplicated with ScriptedPlugin.scriptedParser, this can be // avoided once we upgrade build.properties to 0.13.14 def scriptedParser(scriptedBase: File): Parser[Seq[String]] = import DefaultParsers.* val scriptedFiles: NameFilter = ("test": NameFilter) | "pending" val pairs = (scriptedBase * AllPassFilter * AllPassFilter * scriptedFiles).get() map { (f: File) => val p = f.getParentFile (p.getParentFile.getName, p.getName) } val pairMap = pairs.groupBy(_._1).view.mapValues(_.map(_._2).toSet).toMap val id = charClass(c => !c.isWhitespace && c != '/').+.string val groupP = token(id.examples(pairMap.keySet.toSet)) <~ token('/') // A parser for page definitions val pageNumber = (NatBasic & not('0', "zero page number")).flatMap { i => if i <= pairs.size then Parser.success(i) else Parser.failure(s"$i exceeds the number of tests (${pairs.size})") } val pageP: Parser[ScriptedTestPage] = ("*" ~> pageNumber ~ ("of" ~> pageNumber)) flatMap { case (page, total) if page <= total => success(ScriptedTestPage(page, total)) case (page, total) => failure(s"Page $page was greater than $total") } // Grabs the filenames from a given test group in the current page definition. def pagedFilenames(group: String, page: ScriptedTestPage): Seq[String] = val files = pairMap.get(group).toSeq.flatten.sortBy(_.toLowerCase) val pageSize = if page.total == 0 then files.size else files.size / page.total // The last page may loose some values, so we explicitly keep them val dropped = files.drop(pageSize * (page.page - 1)) if page.page == page.total then dropped else dropped.take(pageSize) def nameP(group: String) = token("*".id | id.examples(pairMap.getOrElse(group, Set.empty[String]))) val PagedIds: Parser[Seq[String]] = for group <- groupP page <- pageP files = pagedFilenames(group, page) // TODO - Fail the parser if we don't have enough files for the given page size // if !files.isEmpty yield files map (f => s"$group/$f") val testID = (for group <- groupP; name <- nameP(group) yield (group, name)) val testIdAsGroup = matched(testID).map(test => Seq(test)) // (token(Space) ~> matched(testID)).* (token(Space) ~> (PagedIds | testIdAsGroup)).*.map(_.flatten) end scriptedParser @nowarn def doScripted( scriptedSbtInstance: ScalaInstance, sourcePath: File, bufferLog: Boolean, args: Seq[String], prescripted: File => Unit, launchOpts: Seq[String], scalaVersion: String, sbtVersion: String, classpath: Seq[File], launcherJar: File, logger: Logger, keepTempDirectory: Boolean, includeFilter: java.io.FileFilter, excludeFilter: java.io.FileFilter, ): Unit = logger.info(s"Tests selected: ${args.mkString("\n * ", "\n * ", "\n")}") logger.info("") val noJLine = new FilteredLoader(scriptedSbtInstance.loader, "jline." :: Nil) val loader = ClasspathUtilities.toLoader(classpath, noJLine) val bridgeClass = Class.forName("sbt.scriptedtest.ScriptedRunner", true, loader) // Interface to cross class loader type SbtScriptedRunner = { def runInParallel( resourceBaseDirectory: File, bufferLog: Boolean, tests: java.util.List[String], launcherJar: File, javaCommand: String, launchOpts: java.util.List[String], prescripted: java.util.List[File], instance: Int, keepTempDirectory: Boolean, includeFilter: java.io.FileFilter, excludeFilter: java.io.FileFilter, ): Unit } val initLoader = Thread.currentThread.getContextClassLoader try Thread.currentThread.setContextClassLoader(loader) val bridge = bridgeClass.getDeclaredConstructor().newInstance().asInstanceOf[SbtScriptedRunner] try // Using java.util.List to encode File => Unit. val callback = new java.util.AbstractList[File]: override def add(x: File): Boolean = prescripted(x); false def get(x: Int): sbt.File = ??? def size(): Int = 0 val instances: Int = (System.getProperty("sbt.scripted.parallel.instances") match case null => 1 case i => scala.util.Try(i.toInt).getOrElse(1) ) match case i if i > 0 => i case _ => 1 import scala.reflect.Selectable.reflectiveSelectable bridge.runInParallel( sourcePath, bufferLog, args.toList.asJava, launcherJar, "java", launchOpts.toList.asJava, callback, instances, keepTempDirectory, includeFilter, excludeFilter, ) catch case ite: InvocationTargetException => throw ite.getCause end try finally Thread.currentThread.setContextClassLoader(initLoader) end try end doScripted end Scripted