| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * license agreements; and to You under the Apache License, version 2.0: |
| * |
| * https://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * This file is part of the Apache Pekko project, which was derived from Akka. |
| */ |
| |
| /* |
| * Copyright (C) 2009-2020 Lightbend Inc. <https://www.lightbend.com> |
| */ |
| |
| import java.io._ |
| |
| import MimaWithPrValidation.{ MimaResult, NoErrors, Problems } |
| import net.virtualvoid.sbt.graph.ModuleGraph |
| import net.virtualvoid.sbt.graph.backend.SbtUpdateReport |
| import org.kohsuke.github.GHIssueComment |
| import org.kohsuke.github.GitHubBuilder |
| import sbt.Keys._ |
| import sbt._ |
| import sbt.access.Aggregation |
| import sbt.access.Aggregation.Complete |
| import sbt.access.Aggregation.KeyValue |
| import sbt.access.Aggregation.ShowConfig |
| import sbt.internal.util.MainAppender |
| import sbt.internal.Act |
| import sbt.internal.BuildStructure |
| import sbt.std.Transform.DummyTaskMap |
| import sbt.util.LogExchange |
| import sbtunidoc.BaseUnidocPlugin.autoImport.unidoc |
| |
| import scala.collection.JavaConverters._ |
| import scala.collection.immutable |
| import scala.language.postfixOps |
| import scala.sys.process._ |
| import scala.util.Try |
| import scala.util.matching.Regex |
| |
| object ValidatePullRequest extends AutoPlugin { |
| |
| override def trigger = allRequirements |
| override def requires = plugins.JvmPlugin |
| |
| sealed trait BuildMode { |
| def task: Option[TaskKey[?]] |
| def log(projectName: String, l: Logger): Unit |
| } |
| |
| case object BuildSkip extends BuildMode { |
| override def task = None |
| def log(projectName: String, l: Logger) = |
| l.info(s"Skipping validation of [$projectName], as PR does NOT affect this project...") |
| } |
| |
| case object BuildQuick extends BuildMode { |
| override def task = Some(ValidatePR / executeTests) |
| def log(projectName: String, l: Logger) = |
| l.info(s"Building [$projectName] in quick mode, as its dependencies were affected by PR.") |
| } |
| |
| case object BuildProjectChangedQuick extends BuildMode { |
| override def task = Some(ValidatePR / executeTests) |
| def log(projectName: String, l: Logger) = |
| l.info(s"Building [$projectName] as the root `project/` directory was affected by this PR.") |
| } |
| |
| final case class BuildCommentForcedAll(phrase: String, c: GHIssueComment) extends BuildMode { |
| override def task = Some(Test / executeTests) |
| def log(projectName: String, l: Logger) = |
| l.info(s"GitHub PR comment [${c.getUrl}] contains [$phrase], forcing BUILD ALL mode!") |
| } |
| |
| val ValidatePR = config("pr-validation").extend(Test) |
| |
| override lazy val projectConfigurations = Seq(ValidatePR) |
| |
| /** |
| * Assumptions: |
| * Env variables set "by Jenkins" are assumed to come from this plugin: |
| * https://wiki.jenkins-ci.org/display/JENKINS/GitHub+pull+request+builder+plugin |
| */ |
| |
| // settings |
| val PullIdEnvVarName = "ghprbPullId" // Set by "GitHub pull request builder plugin" |
| |
| val TargetBranchEnvVarName = "PR_TARGET_BRANCH" |
| val TargetBranchJenkinsEnvVarName = "ghprbTargetBranch" |
| val SourceBranchEnvVarName = "PR_SOURCE_BRANCH" |
| val SourcePullIdJenkinsEnvVarName = "ghprbPullId" // used to obtain branch name in form of "pullreq/17397" |
| |
| val sourceBranch = settingKey[String]("Branch containing the changes of this PR") |
| val targetBranch = settingKey[String]("Target branch of this PR, defaults to `main`") |
| |
| // asking github comments if this PR should be PLS BUILD ALL |
| val gitHubEnforcedBuildAll = |
| taskKey[Option[BuildMode]]("Checks via GitHub API if comments included the PLS BUILD ALL keyword") |
| val buildAllKeyword = taskKey[Regex]( |
| "Magic phrase to be used to trigger building of the entire project instead of analysing dependencies") |
| |
| // determining touched dirs and projects |
| val changedDirectories = taskKey[immutable.Set[String]]("List of touched modules in this PR branch") |
| val validatePRprojectBuildMode = |
| taskKey[BuildMode]("Determines what will run when this project is affected by the PR and should be rebuilt") |
| |
| // running validation |
| val validatePullRequest = taskKey[Unit]("Validate pull request and report aggregated results") |
| val executePullRequestValidation = taskKey[Seq[KeyValue[Result[Any]]]]("Run pull request per project") |
| val additionalTasks = settingKey[Seq[TaskKey[?]]]("Additional tasks for pull request validation") |
| |
| // The set of (top-level) files or directories to watch for build changes. |
| val BuildFilesAndDirectories = Set("project", "build.sbt", ".github") |
| |
| def changedDirectoryIsDependency(changedDirs: Set[String], name: String, |
| graphsToTest: Seq[(Configuration, ModuleGraph)])(log: Logger): Boolean = { |
| val dirsOrExperimental = changedDirs.flatMap(dir => Set(dir, s"$dir-experimental")) |
| graphsToTest.exists { case (ivyScope, deps) => |
| log.debug(s"Analysing [$ivyScope] scoped dependencies...") |
| |
| deps.nodes.foreach { m => log.debug(" -> " + m.id) } |
| |
| // if this project depends on a modified module, we must test it |
| deps.nodes.exists { m => |
| // match just by name, we'd rather include too much than too little |
| val dependsOnModule = dirsOrExperimental.find(m.id.name contains _) |
| val depends = dependsOnModule.isDefined |
| if (depends) log.info(s"Project [$name] must be verified, because depends on [${dependsOnModule.get}]") |
| depends |
| } |
| } |
| } |
| |
| def localTargetBranch: Option[String] = System.getenv.asScala.get("PR_TARGET_BRANCH") |
| def jenkinsTargetBranch: Option[String] = System.getenv.asScala.get("ghprbTargetBranch") |
| def runningOnJenkins: Boolean = jenkinsTargetBranch.isDefined |
| def runningLocally: Boolean = !runningOnJenkins |
| |
| override lazy val buildSettings = Seq( |
| ValidatePR / sourceBranch in Global := { |
| sys.env.get(SourceBranchEnvVarName).orElse( |
| sys.env.get(SourcePullIdJenkinsEnvVarName).map("pullreq/" + _)).getOrElse( // Set by "GitHub pull request builder plugin" |
| "HEAD") |
| }, |
| ValidatePR / targetBranch in Global := { |
| (localTargetBranch, jenkinsTargetBranch) match { |
| case (Some(local), _) => local // local override |
| case (None, Some(branch)) => s"origin/$branch" // usually would be "main" or "release-10.1" etc |
| case (None, None) => "origin/main" // defaulting to diffing with the main branch |
| } |
| }, |
| ValidatePR / buildAllKeyword in Global := """PLS BUILD ALL""".r, |
| ValidatePR / gitHubEnforcedBuildAll in Global := { |
| val log = streams.value.log |
| val buildAllMagicPhrase = (ValidatePR / buildAllKeyword).value |
| sys.env.get(PullIdEnvVarName).map(_.toInt).flatMap { prId => |
| log.info("Checking GitHub comments for PR validation options...") |
| |
| try { |
| import scala.collection.JavaConverters._ |
| val gh = GitHubBuilder.fromEnvironment().withOAuthToken(GitHub.envTokenOrThrow).build() |
| val comments = gh.getRepository("apache/pekko-http").getIssue(prId).getComments.asScala |
| |
| def triggersBuildAll(c: GHIssueComment): Boolean = buildAllMagicPhrase.findFirstIn(c.getBody).isDefined |
| comments.collectFirst { |
| case c if triggersBuildAll(c) => |
| BuildCommentForcedAll(buildAllMagicPhrase.toString(), c) |
| } |
| } catch { |
| case ex: Exception => |
| log.warn("Unable to reach GitHub! Exception was: " + ex.getMessage) |
| None |
| } |
| } |
| }, |
| ValidatePR / changedDirectories in Global := { |
| val log = streams.value.log |
| val prId = (ValidatePR / sourceBranch).value |
| val target = (ValidatePR / targetBranch).value |
| |
| log.info(s"Diffing [$prId] to determine changed modules in PR...") |
| val diffOutput = s"git diff $target --name-only".!!.split("\n") |
| val diffedModuleNames = |
| diffOutput |
| .map(l => l.trim) |
| .filter(l => |
| l.startsWith("http") || |
| l.startsWith("parsing") || |
| l.startsWith("docs") || |
| BuildFilesAndDirectories.exists(l.startsWith)) |
| .map(l => l.takeWhile(_ != '/')) |
| .toSet |
| |
| val dirtyModuleNames: Set[String] = |
| if (runningOnJenkins) Set.empty |
| else { |
| val statusOutput = s"git status --short".!!.split("\n") |
| val dirtyDirectories = statusOutput |
| .map(l => l.trim.dropWhile(_ != ' ').drop(1)) |
| .map(_.takeWhile(_ != '/')) |
| .filter(dir => |
| dir.startsWith("http") || dir.startsWith("parsing") || dir.startsWith("docs") || |
| BuildFilesAndDirectories.contains(dir)) |
| .toSet |
| log.info( |
| "Detected uncommitted changes in directories (including in dependency analysis): " + |
| dirtyDirectories.mkString( |
| "[", ",", "]")) |
| dirtyDirectories |
| } |
| |
| val allModuleNames = dirtyModuleNames ++ diffedModuleNames |
| log.info("Detected changes in directories: " + allModuleNames.mkString("[", ", ", "]")) |
| allModuleNames |
| }) |
| |
| override lazy val projectSettings = inConfig(ValidatePR)(Defaults.testTasks) ++ Seq( |
| ValidatePR / testOptions += Tests.Argument(TestFrameworks.ScalaTest, "-l", "performance"), |
| ValidatePR / testOptions += Tests.Argument(TestFrameworks.ScalaTest, "-l", "long-running"), |
| ValidatePR / testOptions += Tests.Argument(TestFrameworks.ScalaTest, "-l", "timing"), |
| ValidatePR / validatePRprojectBuildMode := { |
| val log = streams.value.log |
| log.debug(s"Analysing project (for inclusion in PR validation): [${name.value}]") |
| val changedDirs = (ValidatePR / changedDirectories).value |
| val githubCommandEnforcedBuildAll = (ValidatePR / gitHubEnforcedBuildAll).value |
| |
| val thisProjectId = CrossVersion(scalaVersion.value, scalaBinaryVersion.value)(projectID.value) |
| |
| def graphFor(updateReport: UpdateReport, config: Configuration): (Configuration, ModuleGraph) = |
| config -> SbtUpdateReport.fromConfigurationReport(updateReport.configuration(config).get, thisProjectId) |
| |
| def isDependency: Boolean = { |
| changedDirectoryIsDependency( |
| changedDirs, |
| name.value, |
| Seq( |
| graphFor((Compile / updateFull).value, Compile), |
| graphFor((Test / updateFull).value, Test), |
| graphFor((Runtime / updateFull).value, Runtime), |
| graphFor((Provided / updateFull).value, Provided), |
| graphFor((Optional / updateFull).value, Optional)))(log) |
| } |
| |
| if (githubCommandEnforcedBuildAll.isDefined) |
| githubCommandEnforcedBuildAll.get |
| else if (changedDirs.exists(BuildFilesAndDirectories contains)) |
| BuildProjectChangedQuick |
| else if (isDependency) |
| BuildQuick |
| else |
| BuildSkip |
| }, |
| ValidatePR / additionalTasks := Seq.empty, |
| executePullRequestValidation := Def.taskDyn { |
| val log = streams.value.log |
| val buildMode = (ValidatePR / validatePRprojectBuildMode).value |
| |
| buildMode.log(name.value, log) |
| |
| val validationTasks: Seq[TaskKey[Any]] = |
| (buildMode.task.toSeq ++ |
| (buildMode match { |
| case BuildSkip => Seq.empty // do not run the additional task if project is skipped during pr validation |
| case _ => (ValidatePR / additionalTasks).value |
| })).asInstanceOf[Seq[TaskKey[Any]]] |
| |
| val thisProject = Def.resolvedScoped.value.scope.project.toOption.get |
| |
| // Create a task for every validation task key and |
| // then zip all of the tasks together discarding outputs. |
| // Task failures are propagated as normal. |
| val zero: Def.Initialize[Seq[Task[KeyValue[Result[Any]]]]] = |
| Def.setting { Seq(task(()).result.map(res => KeyValue(null, res))) } |
| validationTasks.map(taskKey => |
| Def.task { taskKey.value }.result.map(res => KeyValue(thisProject / taskKey, res))).foldLeft(zero) { |
| (acc, current) => |
| acc.zipWith(current) { case (taskSeq, task) => |
| taskSeq :+ task.asInstanceOf[Task[KeyValue[Result[Any]]]] |
| } |
| }.apply { tasks: Seq[Task[KeyValue[Result[Any]]]] => |
| tasks.join |
| } |
| }.value) |
| } |
| |
| object AggregatePRValidation extends AutoPlugin { |
| import ValidatePullRequest._ |
| |
| override def trigger = noTrigger |
| |
| override lazy val projectSettings = Seq( |
| validatePullRequest := { |
| val log = streams.value.log |
| |
| val extracted = Project.extract(state.value) |
| val keys = |
| Aggregation.aggregatedKeys(extracted.currentRef / validatePullRequest, extracted.structure.extra, ScopeMask()) |
| |
| log.info(s"AGGREGATE KEYS for ${extracted.currentRef}") |
| keys.foreach(x => log.info(x.toString)) |
| log.info("END") |
| |
| def timedRun[T]( |
| s: State, ts: Seq[sbt.internal.Aggregation.KeyValue[Task[T]]], extra: DummyTaskMap): Complete[Result[T]] = { |
| import EvaluateTask._ |
| import std.TaskExtra._ |
| |
| val extracted = Project.extract(s) |
| import extracted.structure |
| val toRun: Seq[Task[KeyValue[Result[T]]]] = ts.map { case KeyValue(k, t) => t.result.map(v => KeyValue(k, v)) } |
| val joined = Task[Seq[KeyValue[Result[T]]]](Info(), |
| new Join(toRun, |
| { (s: Seq[Result[KeyValue[Result[T]]]]) => |
| val extracted = s.map { |
| case _: Inc => throw new IllegalStateException("Should not happen") // should not happen because of `result` above |
| case Value(kv) => kv |
| } |
| Right(extracted) |
| })) |
| |
| val roots = ts.map { case KeyValue(k, _) => k } |
| val config = extractedTaskConfig(extracted, structure, s) |
| |
| val start = System.currentTimeMillis |
| val (newS, result: Result[Seq[KeyValue[Result[T]]]]) = withStreams(structure, s) { str => |
| val transform = nodeView(s, str, roots, extra) |
| runTask(joined, s, str, structure.index.triggers, config)(transform) |
| } |
| val stop = System.currentTimeMillis |
| Complete(start, stop, result, newS) |
| } |
| |
| def runTasks[T](s: State, structure: BuildStructure, ts: Seq[sbt.internal.Aggregation.KeyValue[Task[T]]], |
| extra: DummyTaskMap, show: ShowConfig)( |
| implicit display: Show[ScopedKey[?]]): (State, Result[Seq[KeyValue[Result[T]]]]) = { |
| val complete = timedRun[T](s, ts, extra) |
| sbt.access.AggregationShowRun(complete, show) |
| val newState = |
| complete.results match { |
| case Inc(i) => complete.state.handleError(i) |
| case Value(_) => complete.state |
| } |
| |
| (newState, complete.results) |
| } |
| def resolve[T](key: ScopedKey[T]): ScopedKey[T] = |
| Project.mapScope(Scope.resolveScope(GlobalScope, extracted.currentRef.build, extracted.rootProject))( |
| key.scopedKey) |
| |
| def runAggregated[T](key: TaskKey[T], state: State): (State, Result[Seq[KeyValue[Result[T]]]]) = { |
| val rkey = resolve(key.scopedKey) |
| val keys = Aggregation.aggregate(rkey, ScopeMask(), extracted.structure.extra) |
| val tasks = Act.keyValues(extracted.structure)(keys) |
| log.info(s"Tasks to aggregate are: $keys $tasks") |
| |
| runTasks[T](state, extracted.structure, tasks, DummyTaskMap(Nil), show = Aggregation.defaultShow(state, false))( |
| extracted.showKey) |
| } |
| |
| val (newState, result) = runAggregated(extracted.currentRef / executePullRequestValidation, state.value) |
| |
| implicit class AddBetterEitherError[T](val e: Either[sbt.Incomplete, T]) { |
| def getSafe: T = e match { |
| case Left(i) => throw new IllegalStateException(s"Was not Right but [$i]", i.directCause.getOrElse(null)) |
| case Right(t) => t |
| } |
| } |
| |
| val allResults = |
| result |
| .toEither.getSafe |
| .flatMap(_.value.toEither.getSafe) |
| .filterNot(_.key == null) |
| .sortBy(_.key.scope.project.toString) |
| |
| val onlyTestResults: Seq[KeyValue[Tests.Output]] = allResults.collect { |
| case KeyValue(key, Value(o: Tests.Output)) => KeyValue(key, o) |
| } |
| val (passed0, failed) = onlyTestResults.partition(_.value.overall == TestResult.Passed) |
| |
| val failedTasks: Seq[KeyValue[Inc]] = allResults.collect { |
| case KeyValue(key, i: Inc) => KeyValue(key, i) |
| } |
| |
| val mimaFailures: Seq[KeyValue[MimaResult]] = allResults.collect { |
| case KeyValue(key, Value(p: Problems)) => KeyValue(key, p) |
| } |
| |
| val outputFile = new File(target.value, "pr-validation-report.txt") |
| val fw = new PrintWriter(new FileWriter(outputFile)) |
| def write(msg: String): Unit = { |
| log.info(msg) |
| fw.println(msg) |
| } |
| |
| @scala.annotation.nowarn("cat=deprecation") |
| val testLogger = LogExchange.logger("testLogger") |
| val appender = MainAppender.defaultBacked(useFormat = false)(fw) |
| @scala.annotation.nowarn("cat=deprecation") |
| def installTestLoggerAppender(): Unit = |
| LogExchange.bindLoggerAppenders("testLogger", appender -> Level.Info :: Nil) |
| installTestLoggerAppender() |
| |
| log.info("") |
| if (failed.nonEmpty || mimaFailures.nonEmpty || failedTasks.nonEmpty) { |
| write("") |
| write("## Pull request validation report") |
| write("") |
| |
| def showKey(key: ScopedKey[?]): String = Project.showContextKey2(extracted.session).show(key) |
| |
| def totalCount(suiteResult: SuiteResult): Int = { |
| import suiteResult._ |
| passedCount + failureCount + errorCount + skippedCount + ignoredCount + canceledCount + pendingCount |
| } |
| |
| def hasExecutedTests(suiteResult: SuiteResult): Boolean = totalCount(suiteResult) > 0 |
| |
| def hasTests(result: Tests.Output): Boolean = result.events.exists(e => hasExecutedTests(e._2)) |
| |
| def printTestResults(result: KeyValue[Tests.Output]): Unit = { |
| write(s"Test result for `${showKey(result.key)}`") |
| write("") |
| write("```") |
| |
| def safeLogTestResults(logger: Logger): Unit = |
| Try(TestResultLogger.Default.run(logger, result.value, showKey(result.key))) |
| |
| // HACK: there is no logger which would both log to our file and the console, so we log twice |
| safeLogTestResults(log) |
| safeLogTestResults(testLogger) |
| |
| // there seems to be some async logging going on, so let's wait for a while to be sure the appender has flushed |
| Thread.sleep(100) |
| |
| write("```") |
| write("") |
| } |
| |
| val passed = passed0.filter(t => hasTests(t.value)) |
| |
| if (failed.nonEmpty) { |
| write("<details><summary>Failed Test Suites</summary>") |
| write("") |
| failed.foreach(printTestResults) |
| write("</details>") |
| } |
| |
| if (mimaFailures.nonEmpty) { |
| write("<details><summary>Mima Failures</summary>") |
| write("") |
| write("```") |
| mimaFailures.foreach { |
| case KeyValue(key, Problems(desc)) => |
| write(s"Problems for `${key.scope.project.toOption.get.asInstanceOf[ProjectRef].project}`:\n$desc") |
| write("") |
| case KeyValue(_, NoErrors) => |
| } |
| write("```") |
| write("</details>") |
| } |
| |
| if (failedTasks.nonEmpty) { |
| write("<details><summary>Other Failed Tasks</summary>") |
| write("") |
| failedTasks.foreach { case KeyValue(key, Inc(inc: Incomplete)) => |
| def parseIncomplete(inc: Incomplete): String = |
| "an underlying problem during task execution:\n" + |
| Incomplete.linearize(inc).filter(x => x.message.isDefined || x.directCause.isDefined) |
| .map { case i @ Incomplete(node, tpe, message, causes, directCause) => |
| def nodeName: String = node match { |
| case Some(key: ScopedKey[?]) => showKey(key) |
| case Some(t: Task[?]) => |
| t.info.name |
| .orElse(t.info.attributes.get(taskDefinitionKey).map(showKey)) |
| .getOrElse(t.info.toString) |
| case Some(x) => s"<$x>" |
| case None => "<unknown>" |
| } |
| |
| s" $nodeName: ${message.orElse(directCause.map(_.toString)).getOrElse(s"<unknown: ($i)>")}" |
| }.mkString("```\n", "\n", "\n```\n") |
| |
| val problem = inc.directCause.map(_.toString).getOrElse(parseIncomplete(inc)) |
| |
| write(s"`${showKey(key)}` failed because of $problem") |
| } |
| write("</details>") |
| } |
| } |
| |
| fw.close() |
| log.info(s"Wrote PR validation report to ${outputFile.getAbsolutePath}") |
| |
| if (failed.nonEmpty) throw new RuntimeException(s"Pull request validation failed! Tests failed: $failed") |
| else if (mimaFailures.nonEmpty) |
| throw new RuntimeException(s"Pull request validation failed! Mima failures: $mimaFailures") |
| else if (failedTasks.nonEmpty) |
| throw new RuntimeException(s"Pull request validation failed! Failed tasks: $failedTasks") |
| () |
| }) |
| } |
| |
| /** |
| * This auto plugin adds MiMa binary issue reporting to validatePullRequest task, |
| * when a project has MimaPlugin auto plugin enabled. |
| */ |
| object MimaWithPrValidation extends AutoPlugin { |
| import ValidatePullRequest._ |
| import com.typesafe.tools.mima.plugin._ |
| import MimaKeys._ |
| |
| sealed trait MimaResult |
| case object NoErrors extends MimaResult |
| case class Problems(problemDescription: String) extends MimaResult |
| |
| val mimaResult = taskKey[MimaResult]("Aggregates mima result strings") |
| |
| override def trigger = allRequirements |
| override def requires = ValidatePullRequest && MimaPlugin |
| override lazy val projectSettings = Seq( |
| ValidatePR / additionalTasks += mimaResult, |
| mimaResult := { |
| if (scalaVersion.value == Dependencies.scala3NextVersion) NoErrors |
| else { |
| import com.typesafe.tools.mima.core |
| def reportModuleErrors(module: ModuleID, |
| backward: List[core.Problem], |
| forward: List[core.Problem], |
| filters: Seq[core.ProblemFilter], |
| backwardFilters: Map[String, Seq[core.ProblemFilter]], |
| forwardFilters: Map[String, Seq[core.ProblemFilter]], |
| log: String => Unit, projectName: String): Boolean = { |
| // filters * found is n-squared, it's fixable in principle by special-casing known |
| // filter types or something, not worth it most likely... |
| |
| val versionOrdering = { |
| // version string "x.y.z" is converted to an Int tuple (x, y, z) for comparison |
| val VersionRegex = """(\d+)\.?(\d+)?\.?(.*)?""".r |
| def int(versionPart: String) = |
| Try(versionPart.replace("x", Short.MaxValue.toString).filter(_.isDigit).toInt).getOrElse(0) |
| Ordering[(Int, Int, Int)].on[String] { case VersionRegex(x, y, z) => (int(x), int(y), int(z)) } |
| } |
| def isReported(module: ModuleID, verionedFilters: Map[String, Seq[core.ProblemFilter]])( |
| problem: core.Problem) = |
| (verionedFilters.collect { |
| // get all filters that apply to given module version or any version after it |
| case f @ (version, filters) if versionOrdering.gteq(version, module.revision) => filters |
| }.flatten ++ filters).forall { f => |
| if (f(problem)) { |
| true |
| } else { |
| // log(projectName + ": filtered out: " + problem.description + "\n filtered by: " + f) |
| false |
| } |
| } |
| |
| val backErrors = backward.filter(isReported(module, backwardFilters)) |
| val forwErrors = forward.filter(isReported(module, forwardFilters)) |
| |
| val filteredCount = backward.size + forward.size - backErrors.size - forwErrors.size |
| val filteredNote = if (filteredCount > 0) " (filtered " + filteredCount + ")" else "" |
| |
| // TODO - Line wrapping an other magikz |
| def prettyPrint(p: core.Problem, affected: String): String = { |
| " * " + p.description(affected) + p.howToFilter.map("\n filter with: " + _).getOrElse("") |
| } |
| |
| log( |
| s"$projectName: found ${backErrors.size + forwErrors.size} potential binary incompatibilities while checking against $module $filteredNote") |
| ((backErrors.map { p: core.Problem => prettyPrint(p, "current") }) ++ |
| (forwErrors.map { p: core.Problem => prettyPrint(p, "other") })).foreach { p => |
| log(p) |
| } |
| backErrors.nonEmpty || forwErrors.nonEmpty |
| } |
| |
| def withLogger[T](f: (String => Unit) => T): (String, T) = { |
| val stringWriter = new StringWriter() |
| val printWriter = new PrintWriter(stringWriter) |
| |
| val result = f(printWriter.println) |
| |
| printWriter.close() |
| stringWriter.close() |
| (stringWriter.toString, result) |
| } |
| |
| val allResults = |
| mimaPreviousClassfiles.value.toSeq.map { |
| case (moduleId, file) => |
| val problems = SbtMima.runMima( |
| file, |
| mimaCurrentClassfiles.value, |
| (mimaFindBinaryIssues / fullClasspath).value, |
| mimaCheckDirection.value, |
| scalaVersion.value, |
| streams.value.log, |
| Nil) |
| |
| val binary = mimaBinaryIssueFilters.value |
| val backward = mimaBackwardIssueFilters.value |
| val forward = mimaForwardIssueFilters.value |
| |
| withLogger { logger => |
| reportModuleErrors( |
| moduleId, |
| problems._1, problems._2, |
| binary, |
| backward, |
| forward, |
| logger, |
| name.value) |
| } |
| } |
| |
| val noErrors = allResults.forall(!_._2) |
| if (noErrors) NoErrors |
| else { |
| val erroneous = allResults.filter(_._2) |
| Problems(erroneous.map(_._1).mkString("\n")) |
| } |
| } |
| }) |
| } |
| |
| /** |
| * This auto plugin adds UniDoc unification to validatePullRequest task. |
| */ |
| object UniDocWithPrValidation extends AutoPlugin { |
| import ValidatePullRequest._ |
| |
| override def trigger = noTrigger |
| override lazy val projectSettings = Seq( |
| ValidatePR / additionalTasks += Compile / unidoc) |
| } |